diff --git a/.codebuddy/skills/tdesign-webcomponents-dev/SKILL.md b/.codebuddy/skills/tdesign-webcomponents-dev/SKILL.md new file mode 100644 index 00000000..c682ca18 --- /dev/null +++ b/.codebuddy/skills/tdesign-webcomponents-dev/SKILL.md @@ -0,0 +1,218 @@ +--- +name: TDesign Web Components 开发助手 +description: | + 辅助 TDesign Web Components 组件库的开发和维护。该组件库基于 Omi 框架开发,API 规范和组件实现需参考 TDesign-React。 + 此 skill 应在以下场景使用: + - 开发新的 TDesign Web Components 组件 + - 维护或修复现有组件 + - 需要确保组件在 Omi 和非 Omi 环境(如 React)中行为一致 + - 需要了解组件的 API 设计规范、类型定义、样式规范 + - 使用 omi-reactify 包装器适配 React 环境 +--- + +# TDesign Web Components 开发助手 + +## 概述 + +TDesign Web Components 是腾讯 TDesign 设计系统的 Web Components 实现版本,基于 Omi 框架开发。 + +**核心要求**: +1. API 设计、Props 命名、类型定义需与 TDesign-React 保持一致 +2. 组件在 Omi 和非 Omi 环境(React/Vue/原生 JS)中行为、样式一致 +3. 使用 omi-reactify 为非 Omi 环境提供一致的调用方式 + +--- + +## 组件目录结构 + +``` +src/[component-name]/ +├── [component-name].tsx # 组件主实现 +├── index.ts # 导出入口 +├── type.ts # TypeScript 类型定义 +├── style/index.js # 样式导入 +└── _example/ # 示例代码 +``` + +--- + +## 组件实现模板 + +```tsx +import { Component, tag } from 'omi'; +import classname, { getClassPrefix } from '../_util/classname'; +import { createEmit } from '../_util/emit'; +import { setExportparts } from '../_util/component'; +import { TdXxxProps } from './type'; +import { StyledProps } from '../common'; + +export interface XxxProps extends TdXxxProps, StyledProps {} + +@tag('t-xxx') +export default class Xxx extends Component { + static css = []; + static propTypes = { theme: String, size: String, disabled: Boolean }; + static defaultProps: Partial = { size: 'medium', disabled: false }; + + private innerValue = ''; + private emit = createEmit(this); + + install() { + this.innerValue = this.props.value ?? this.props.defaultValue ?? ''; + } + + ready() { + setExportparts(this); + } + + receiveProps(props: XxxProps, oldProps: XxxProps) { + if (props.value !== undefined && props.value !== oldProps.value) { + this.innerValue = props.value; + } + } + + handleClick = (e: MouseEvent) => { + if (this.props.disabled) return; + this.props.onClick?.(e); // Omi 环境 + this.emit('click', { e }); // 非 Omi 环境 + }; + + render(props: XxxProps) { + const classPrefix = getClassPrefix(); + const value = props.value !== undefined ? props.value : this.innerValue; + + return ( +
+ {props.children} +
+ ); + } +} +``` + +--- + +## 生命周期 + +| 生命周期 | 触发时机 | 用途 | +|---------|---------|------| +| `install()` | 实例化时(render 前) | 初始化内部状态 | +| `installed()` | 首次 DOM 挂载后 | 添加全局事件监听 | +| `ready()` | DOM 准备就绪 | setExportparts、初始化 Observer | +| `receiveProps(props, old)` | props 变化时 | 同步受控状态 | +| `beforeRender()` | 每次渲染前 | Light DOM 样式注入 | +| `uninstall()` | 组件卸载时 | 清理资源 | + +**关键差异**:非 Omi 环境中 `receiveProps` 不会自动调用,受控模式需特别处理。 + +详见 [references/lifecycle.md](references/lifecycle.md) + +--- + +## 核心工具函数 + +| 工具 | 用途 | 示例 | +|------|------|------| +| `useControlled` | 受控/非受控模式 | `const [val, onChange] = useControlled(props, 'value', handler, { activeComponent: this })` | +| `parseTNode` | 解析 TNode(函数/组件/字符串) | `parseTNode(props.icon, { size: 'small' })` | +| `hasSlot` / `getSlotNodes` | Slot 检测与获取 | `hasSlot('icon', this.props.children)` | +| `convertToLightDomNode` | Light DOM 模式(插件组件) | `render(convertToLightDomNode(), container)` | +| `classname` | 类名生成 | `classname(className, 't-btn', { 't-is-disabled': disabled })` | +| `createEmit` | 事件派发(基于 `fire` 封装) | `this.emit('change', { value, e })` | + +详见 [references/utils.md](references/utils.md) + +--- + +## 事件处理 + +组件需同时支持 Omi 和非 Omi 环境,使用双重派发模式(见组件模板)。 + +--- + +## API 设计规范 + +### 命名规范 + +| 类型 | 规范 | 示例 | +|------|------|------| +| Props | 小驼峰 | `disabled`, `defaultValue` | +| 事件 | on + 动词 | `onClick`, `onChange` | +| CSS | BEM | `t-button`, `t-is-disabled` | +| 标签 | t- 前缀 | `` | + +### 受控/非受控 + +| 受控 | 非受控 | 回调 | +|-----|-------|------| +| `value` | `defaultValue` | `onChange` | +| `visible` | `defaultVisible` | `onVisibleChange` | + +--- + +## 样式规范 + +样式来自 `_common` 子仓库(tdesign-common): + +```javascript +// style/index.js +import { css, globalCSS } from 'omi'; +import styles from '../../_common/style/web/components/xxx/_index.less'; +export const styleSheet = css`${styles}`; +globalCSS(styleSheet); +``` + +CSS 类名规范:`t-{component}`, `t-{component}--{modifier}`, `t-is-{state}` + +--- + +## 跨环境一致性 + +### omi-reactify 包装器 + +```tsx +import reactify from 'omi-reactify'; +const TButton = reactify('t-button'); + +// React 中使用 +按钮 +``` + +### Slot 声明 + +有自定义 slot 时需声明 `slotProps`: + +```typescript +@tag('t-select') +export default class Select extends Component { + static slotProps = ['prefixIcon', 'suffixIcon', 'panel']; +} +``` + +详见 [references/omi-reactify.md](references/omi-reactify.md) + +--- + +## 参考资料 + +- [生命周期详解](references/lifecycle.md) - Omi vs 非 Omi 环境差异 +- [工具函数详解](references/utils.md) - useControlled、parseTNode、lightDom 等 +- [omi-reactify 参考](references/omi-reactify.md) - React 环境适配 +- [TDesign-React 模式](references/tdesign-react-patterns.md) - API 对照参考 + +--- + +## 开发检查清单 + +- [ ] Props/类型与 TDesign-React 一致 +- [ ] 支持受控/非受控模式 +- [ ] 双重事件派发(Props 回调 + emit) +- [ ] 声明 slotProps(如有自定义 slot) +- [ ] ready() 中调用 setExportparts +- [ ] 样式效果与 React 版本一致 diff --git a/.codebuddy/skills/tdesign-webcomponents-dev/references/lifecycle.md b/.codebuddy/skills/tdesign-webcomponents-dev/references/lifecycle.md new file mode 100644 index 00000000..b20e9fac --- /dev/null +++ b/.codebuddy/skills/tdesign-webcomponents-dev/references/lifecycle.md @@ -0,0 +1,122 @@ +# 组件生命周期详解 + +## Omi 生命周期方法 + +| 生命周期 | 触发时机 | 典型用途 | +|---------|---------|---------| +| `install()` | 组件实例化时(render 前) | 初始化内部状态、设置默认值 | +| `installed()` | 首次 DOM 挂载后 | 启动定时器、添加全局事件监听 | +| `ready()` | DOM 准备就绪 | 设置 exportparts、初始化 Observer | +| `receiveProps(props, oldProps)` | props 变化时 | 同步受控状态、响应外部变化 | +| `beforeRender()` | 每次渲染前 | Light DOM 样式注入 | +| `rendered()` | 每次渲染后 | 更新 ResizeObserver | +| `uninstall()` | 组件卸载时 | 清理定时器、移除监听、断开 Observer | + +--- + +## 完整生命周期示例 + +```typescript +@tag('t-xxx') +export default class Xxx extends Component { + private innerValue: string = ''; + private resizeObserver: ResizeObserver | null = null; + private emit = createEmit(this); + + // 1. 初始化(render 前) + install() { + this.innerValue = this.props.value ?? this.props.defaultValue ?? ''; + } + + // 2. DOM 挂载后 + installed() { + window.addEventListener('resize', this.handleResize); + } + + // 3. DOM 准备就绪 + ready() { + setExportparts(this); + this.resizeObserver = new ResizeObserver(this.handleResize); + this.resizeObserver.observe(this); + } + + // 4. Props 变化响应(关键:受控模式同步) + receiveProps(props: XxxProps, oldProps: XxxProps) { + if (props.value !== undefined && props.value !== oldProps.value) { + this.innerValue = props.value; + } + } + + // 5. 渲染前(Light DOM 场景) + beforeRender() { + // Light DOM 样式注入逻辑 + } + + // 6. 渲染后 + rendered() { + // 更新 Observer 等 + } + + // 7. 组件卸载清理 + uninstall() { + window.removeEventListener('resize', this.handleResize); + this.resizeObserver?.disconnect(); + } + + render(props: XxxProps) { + const currentValue = props.value !== undefined ? props.value : this.innerValue; + // ... + } +} +``` + +--- + +## Omi vs 非 Omi 环境差异 + +| 场景 | Omi 环境 | 非 Omi 环境(React/Vue/原生) | +|------|---------|---------------------------| +| Props 传递 | 直接通过 JSX 属性 | 通过 DOM property/attribute | +| Props 变化 | `receiveProps` 自动触发 | 需要外部重新设置 property | +| 事件处理 | `this.props.onClick?.()` | 通过 `addEventListener` 监听 | +| 事件派发 | 无需额外处理 | 必须调用 `this.emit()` 派发原生事件 | +| 状态更新 | `this.update()` 触发重渲染 | 同左,但需确保事件已派发 | + +--- + +## 关键注意事项 + +### 1. 非 Omi 环境的 receiveProps + +在非 Omi 环境中,`receiveProps` **不会被自动调用**。这意味着: + +- 受控模式下,外部通过 DOM property 设置 `value` 时,组件不会自动响应 +- 需要依赖 `useControlled` 工具或手动处理 + +### 2. 受控模式实现要点 + +```typescript +// 判断是否受控 +const isControlled = props.value !== undefined; + +// 获取当前值 +const currentValue = isControlled ? props.value : this.innerValue; + +// 值变更时 +handleChange = (newValue) => { + if (!isControlled) { + this.innerValue = newValue; + this.update(); + } + this.props.onChange?.(newValue); + this.emit('change', { value: newValue }); +}; +``` + +### 3. 清理资源 + +`uninstall()` 中必须清理: +- 全局事件监听(window/document) +- 定时器(setTimeout/setInterval) +- Observer(ResizeObserver/MutationObserver/IntersectionObserver) +- 外部订阅 diff --git a/.codebuddy/skills/tdesign-webcomponents-dev/references/omi-reactify.md b/.codebuddy/skills/tdesign-webcomponents-dev/references/omi-reactify.md new file mode 100644 index 00000000..b65f8148 --- /dev/null +++ b/.codebuddy/skills/tdesign-webcomponents-dev/references/omi-reactify.md @@ -0,0 +1,273 @@ +# omi-reactify 包装器参考 + +## 概述 + +`omi-reactify` 是一个将 Web Components 包装为 React 组件的工具,确保 TDesign Web Components 在 React 环境中能够正常工作。 + +## 核心功能 + +### 1. 事件处理 + +将 React 的 `onXxx` 事件转换为 Web Component 的原生事件监听: + +```typescript +// React 中使用 + console.log(e)}>按钮 + +// 内部转换 +// onClick -> 监听 'click' 事件 +// onValueChange -> 监听 'valueChange' 事件 +``` + +**转换规则:** +- `onXxx` -> 监听 `xxx` 事件(首字母小写) +- 事件名保持驼峰命名 + +### 2. Slot 处理 + +将 React 组件渲染到 Web Component 的 `` 中: + +```typescript +// React 中使用 +} /> + +// 内部处理 +// 1. 检测 prefixIcon 是 React 元素 +// 2. 创建
容器 +// 3. 使用 createRoot 将 React 组件渲染到容器中 +``` + +**Slot 检测逻辑:** +1. 组件类上声明的 `slotProps` +2. prop 名以 `Slot` 结尾 +3. shadow DOM 中存在对应的 `` + +### 3. Props 传递 + +根据 prop 类型选择不同的传递方式: + +| 类型 | 传递方式 | +|------|---------| +| string/number/boolean | HTML attribute | +| object/array | DOM property | +| function(非事件) | DOM property | +| style 对象 | 转换为字符串后设置 attribute | + +### 4. Ref 转发 + +支持 React ref 获取底层 Web Component 元素: + +```typescript +const buttonRef = useRef(null); +按钮 + +// buttonRef.current 指向 元素 +``` + +## 源码关键实现 + +### reactify 函数签名 + +```typescript +const reactify = ( + WC: string // Web Component 标签名 +): React.ForwardRefExoticComponent<...> +``` + +### 事件处理实现 + +```typescript +// 检测事件处理器 +if (typeof val === 'function' && prop.match(/^on[A-Za-z]/)) { + const eventName = prop.slice(2); // 去掉 'on' + const omiEventName = eventName[0].toLowerCase() + eventName.slice(1); + this.setEvent(omiEventName, val as EventListener); + return; +} +``` + +### Slot 渲染实现 + +```typescript +renderReactNodeToSlot(reactNode: React.ReactNode, slotName: string) { + // 1. 获取或创建 slot 容器 + let container = webComponent.querySelector(`[slot="${slotName}"]`); + if (!container) { + container = document.createElement('div'); + container.style.display = 'contents'; + container.setAttribute('slot', slotName); + webComponent.appendChild(container); + } + + // 2. 使用 React 18 的 createRoot 渲染 + const root = createRoot(container); + root.render(reactNode); +} +``` + +### 样式对象转换 + +```typescript +const styleObjectToString = (style: any) => { + if (typeof style === 'string') return style; + return Object.keys(style) + .reduce((acc, key) => { + const cssKey = key.replace(/\B([A-Z])/g, '-$1').toLowerCase(); + return acc.concat(`${cssKey}:${style[key]}`); + }, []) + .join(';'); +}; +``` + +## 组件开发注意事项 + +### 1. 声明 slotProps + +如果组件有自定义 slot,需要在组件类上声明: + +```typescript +@tag('t-select') +export default class Select extends Component { + // 声明哪些 props 应该作为 slot 处理 + static slotProps = ['prefixIcon', 'suffixIcon', 'panel', 'empty']; +} +``` + +### 2. 事件命名一致性 + +确保组件派发的事件名与 React 约定一致: + +```typescript +// 组件内部 +this.emit('change', { value }); // 派发 'change' 事件 + +// React 中使用 + console.log(detail.value)} /> +``` + +### 3. 函数类型 Slot + +支持函数类型的 slot(用于动态内容): + +```typescript +// React 中使用 + ( +
{options.map(opt =>
{opt.label}
)}
+ )} +/> + +// 组件内部处理 +if (typeof val === 'function') { + webComponent[prop] = (params) => { + const reactNode = val(params); + return this.renderReactNodeToSlot(reactNode, prop); + }; +} +``` + +### 4. React 19 兼容性 + +omi-reactify 检测 React 版本并适配: + +```typescript +const isReact19Plus = () => { + const version = React.version.split('.')[0]; + return parseInt(version, 10) >= 19; +}; + +// React 19+ 某些属性可以直接通过 attribute 传递 +if (!isReact19Plus()) { + (this.ref.current as any)[prop] = val; +} +``` + +## 使用示例 + +### 基础使用 + +```tsx +import reactify from 'omi-reactify'; +import 'tdesign-web-components/lib/button'; + +const TButton = reactify('t-button'); + +function App() { + return ( + console.log('clicked')} + > + 按钮 + + ); +} +``` + +### 带 Slot 的组件 + +```tsx +import reactify from 'omi-reactify'; +import 'tdesign-web-components/lib/input'; + +const TInput = reactify('t-input'); + +function App() { + return ( + } + suffixIcon={} + onChange={(detail) => console.log(detail.value)} + /> + ); +} +``` + +### 受控组件 + +```tsx +import { useState } from 'react'; +import reactify from 'omi-reactify'; + +const TInput = reactify('t-input'); + +function App() { + const [value, setValue] = useState(''); + + return ( + setValue(detail.value)} + /> + ); +} +``` + +## 调试技巧 + +### 1. 检查 Slot 是否正确挂载 + +```javascript +const el = document.querySelector('t-select'); +console.log(el.querySelectorAll('[slot]')); // 查看所有 slot 容器 +``` + +### 2. 检查事件监听 + +```javascript +const el = document.querySelector('t-button'); +// 使用 Chrome DevTools 的 Event Listeners 面板查看 +``` + +### 3. 检查 Props 传递 + +```javascript +const el = document.querySelector('t-input'); +console.log({ + value: el.value, + disabled: el.disabled, + // 检查 DOM property 而非 attribute +}); +``` diff --git a/.codebuddy/skills/tdesign-webcomponents-dev/references/tdesign-react-patterns.md b/.codebuddy/skills/tdesign-webcomponents-dev/references/tdesign-react-patterns.md new file mode 100644 index 00000000..af060b45 --- /dev/null +++ b/.codebuddy/skills/tdesign-webcomponents-dev/references/tdesign-react-patterns.md @@ -0,0 +1,390 @@ +# TDesign-React 组件模式参考 + +## 概述 + +TDesign Web Components 的 API 设计和组件实现需要参考 TDesign-React。本文档总结了 React 版本的关键模式,供开发时对照。 + +## 组件基本模式 + +### React 版本标准结构 + +```tsx +import React, { forwardRef, useMemo } from 'react'; +import classNames from 'classnames'; +import useConfig from '../hooks/useConfig'; +import useDefaultProps from '../hooks/useDefaultProps'; +import { TdButtonProps } from './type'; +import { buttonDefaultProps } from './defaultProps'; + +export interface ButtonProps + extends TdButtonProps, + Omit, 'content' | 'type'> {} + +const Button = forwardRef((originProps: ButtonProps, ref) => { + const props = useDefaultProps(originProps, buttonDefaultProps); + const { classPrefix } = useConfig(); + const { theme, variant, disabled, children, className, ...rest } = props; + + const buttonClass = classNames( + className, + `${classPrefix}-button`, + `${classPrefix}-button--theme-${theme}`, + { [`${classPrefix}-is-disabled`]: disabled } + ); + + return ( + + ); +}); + +Button.displayName = 'Button'; +export default Button; +``` + +### Web Components 对应实现 + +```tsx +import { Component, tag } from 'omi'; +import classname, { getClassPrefix } from '../_util/classname'; +import { TdButtonProps } from './type'; +import { StyledProps } from '../common'; + +export interface ButtonProps extends TdButtonProps, StyledProps {} + +@tag('t-button') +export default class Button extends Component { + static propTypes = { + theme: String, + variant: String, + disabled: Boolean, + }; + + static defaultProps: Partial = { + variant: 'base', + size: 'medium', + disabled: false, + }; + + render(props: ButtonProps) { + const classPrefix = getClassPrefix(); + const { theme, variant, disabled, children, className } = props; + + const buttonClass = classname( + className, + `${classPrefix}-button`, + `${classPrefix}-button--theme-${theme}`, + { [`${classPrefix}-is-disabled`]: disabled } + ); + + return ( + + ); + } +} +``` + +## 受控/非受控模式 + +### React 版本:useControlled Hook + +```typescript +// React 版本使用 useControlled hook +const [value, onChange] = useControlled(props, 'value', props.onChange); + +// useControlled 实现 +const useControlled = (props, valueKey, onChange, defaultOptions = {}) => { + const controlled = Reflect.has(props, valueKey); + const value = props[valueKey]; + const defaultValue = props[`default${upperFirst(valueKey)}`]; + + const [internalValue, setInternalValue] = useState(defaultValue); + + if (controlled) return [value, onChange || noop]; + + return [ + internalValue, + (newValue, ...args) => { + setInternalValue(newValue); + onChange?.(newValue, ...args); + }, + ]; +}; +``` + +### Web Components 对应实现 + +```typescript +@tag('t-input') +export default class Input extends Component { + // 内部状态 + private internalValue: string = ''; + + // 判断是否受控 + private get isControlled() { + return this.props.value !== undefined; + } + + // 获取当前值 + private get currentValue() { + return this.isControlled ? this.props.value : this.internalValue; + } + + install() { + // 初始化内部值 + this.internalValue = this.props.defaultValue ?? ''; + } + + handleChange = (e: Event) => { + const newValue = (e.target as HTMLInputElement).value; + + // 非受控模式:更新内部状态 + if (!this.isControlled) { + this.internalValue = newValue; + this.update(); + } + + // 触发回调 + this.props.onChange?.(newValue, { e }); + this.emit('change', { value: newValue, e }); + }; + + render() { + return ; + } +} +``` + +## 复合组件模式 + +### React 版本:forwardRefWithStatics + +```tsx +// React 版本 +import forwardRefWithStatics from '../_util/forwardRefWithStatics'; + +const Select = forwardRefWithStatics( + (props: SelectProps, ref) => { + // 组件实现 + }, + { Option, OptionGroup } // 静态属性 +); + +// 使用 +选项1 +``` + +### Web Components 对应实现 + +```typescript +// 主组件 +@tag('t-select') +export default class Select extends Component { + // ... +} + +// 子组件 +@tag('t-option') +export class Option extends Component { + // ... +} + +@tag('t-option-group') +export class OptionGroup extends Component { + // ... +} + +// 导出 +export { Select, Option, OptionGroup }; +export default Select; + +// 使用 + + 选项1 + +``` + +## 通用类型定义 + +### common.ts 对照 + +```typescript +// React 版本 +export type TNode = T extends undefined + ? ReactNode + : ReactNode | ((props: T) => ReactNode); + +export type TElement = T extends undefined + ? ReactElement + : (props: T) => ReactElement; + +// Web Components 版本 +import { VNode, WeElement } from 'omi'; + +export type TNode = VNode | ((props: T) => VNode) | object | string | number | boolean | null; + +export type TElement = T extends undefined + ? WeElement + : (props: T) => WeElement; +``` + +### 尺寸和状态枚举 + +```typescript +// 两个版本保持一致 +export type SizeEnum = 'small' | 'medium' | 'large'; + +export interface StyledProps { + className?: string; + style?: CSSProperties | string; +} +``` + +## Hooks 对应实现 + +### useConfig -> getClassPrefix + +```typescript +// React +const { classPrefix } = useConfig(); + +// Web Components +import { getClassPrefix } from '../_util/classname'; +const classPrefix = getClassPrefix(); // 't' +``` + +### useGlobalIcon -> 直接使用 Icon 组件 + +```typescript +// React +const { CloseIcon } = useGlobalIcon({ CloseIcon: TdCloseIcon }); + +// Web Components +import 'tdesign-icons-web-components/esm/components/close'; +// 直接使用 +``` + +### useRipple -> CSS 实现 + +```typescript +// React 使用 JS 实现波纹效果 +useRipple(ref?.current); + +// Web Components 通过 CSS 实现 +// 样式已包含在 _common/style 中 +``` + +## 事件回调签名 + +### 保持一致的签名 + +```typescript +// 简单事件 +onClick?: (e: MouseEvent) => void; + +// 带值变更 +onChange?: (value: T, context: { e: Event }) => void; + +// 复杂上下文 +onChange?: ( + value: SelectValue, + context: { + option?: T; + selectedOptions: T[]; + trigger: SelectValueChangeTrigger; + e?: MouseEvent | KeyboardEvent; + } +) => void; +``` + +## 类名生成对照 + +### React 版本 + +```typescript +import classNames from 'classnames'; + +const buttonClass = classNames( + className, + `${classPrefix}-button`, + `${classPrefix}-button--theme-${theme}`, + { + [`${classPrefix}-is-disabled`]: disabled, + [`${classPrefix}-is-loading`]: loading, + } +); +``` + +### Web Components 版本 + +```typescript +import classname from '../_util/classname'; + +const buttonClass = classname( + className, + `${classPrefix}-button`, + `${classPrefix}-button--theme-${theme}`, + { + [`${classPrefix}-is-disabled`]: disabled, + [`${classPrefix}-is-loading`]: loading, + } +); +``` + +## 插件模式(命令式调用) + +### React 版本 + +```typescript +// DialogPlugin +const dialog = DialogPlugin.confirm({ + header: '标题', + body: '内容', + onConfirm: () => {}, +}); + +dialog.show(); +dialog.hide(); +dialog.destroy(); +``` + +### Web Components 版本 + +```typescript +// 通过 JS API 实现 +import { DialogPlugin } from 'tdesign-web-components'; + +const dialog = DialogPlugin.confirm({ + header: '标题', + body: '内容', + onConfirm: () => {}, +}); +``` + +## 开发检查清单 + +开发新组件时,对照 React 版本检查: + +### Props 一致性 +- [ ] Props 名称相同 +- [ ] Props 类型相同 +- [ ] 默认值相同 +- [ ] 必填/可选一致 + +### 事件一致性 +- [ ] 事件名称相同 +- [ ] 回调签名相同 +- [ ] context 参数一致 + +### 功能一致性 +- [ ] 受控/非受控模式 +- [ ] 键盘交互 +- [ ] 无障碍支持 + +### 样式一致性 +- [ ] 类名生成逻辑相同 +- [ ] 状态类名相同 +- [ ] 视觉效果一致 diff --git a/.codebuddy/skills/tdesign-webcomponents-dev/references/utils.md b/.codebuddy/skills/tdesign-webcomponents-dev/references/utils.md new file mode 100644 index 00000000..381ed59c --- /dev/null +++ b/.codebuddy/skills/tdesign-webcomponents-dev/references/utils.md @@ -0,0 +1,195 @@ +# 核心工具函数详解 + +## useControlled - 受控/非受控模式 + +处理组件的受控/非受控双模式,自动判断并维护状态。 + +### 基本用法 + +```typescript +import useControlled from '../_util/useControlled'; + +@tag('t-input') +export default class Input extends Component { + render(props: InputProps) { + const [value, onChange] = useControlled(props, 'value', this.handleChange, { + defaultValue: props.defaultValue, + activeComponent: this, // 传入组件实例以支持响应式 + }); + + return onChange(e.target.value, { e })} />; + } + + handleChange = (value: string, context: { e: Event }) => { + this.props.onChange?.(value, context); + this.emit('change', { value, e: context.e }); + }; +} +``` + +### 工作原理 + +1. 通过 `Reflect.has(props, valueKey)` 判断是否受控 +2. **受控模式**:直接返回 `props.value` +3. **非受控模式**:使用 `signal` 维护内部状态,初始值为 `defaultValue` + +### 参数说明 + +| 参数 | 类型 | 说明 | +|------|------|------| +| `props` | object | 组件 props | +| `valueKey` | string | 受控属性名(如 'value') | +| `onChange` | function | 值变更回调 | +| `options.defaultValue` | any | 默认值 | +| `options.activeComponent` | Component | 组件实例(用于触发更新) | + +--- + +## parseTNode - TNode 解析 + +解析 TNode 类型(函数/组件/字符串),统一处理自定义内容渲染。 + +### 基本用法 + +```typescript +import parseTNode, { parseContentTNode } from '../_util/parseTNode'; + +// 基础解析:处理函数类型的 TNode +const node = parseTNode(props.icon, { size: 'small' }); + +// 带 props 注入的解析:支持组件类型 +const content = parseContentTNode(props.content, { disabled: true }); +``` + +### 支持的类型 + +| 类型 | 示例 | 处理方式 | +|------|------|---------| +| 函数 | `content={(props) => }` | 调用函数获取节点 | +| 组件 | `content={}` | 使用 `cloneElement` 注入 props | +| 基础类型 | 字符串、数字、布尔值 | 直接返回 | + +### parseTNode vs parseContentTNode + +- `parseTNode`:仅处理函数类型,传入参数调用 +- `parseContentTNode`:额外支持组件类型,会注入 props + +--- + +## component 工具 - Slot 处理 + +处理 Web Components 的 slot 相关操作。 + +### hasSlot - 检测 slot 是否存在 + +```typescript +import { hasSlot } from '../_util/component'; + +if (hasSlot('icon', this.props.children)) { + // 渲染带 icon 的布局 +} +``` + +### getSlotNodes - 获取所有 slot 节点 + +```typescript +import { getSlotNodes } from '../_util/component'; + +const slotNodes = getSlotNodes(this.props.children); +// 返回 { default: [...], icon: [...], ... } +``` + +### convertNodeListToVNodes - DOM 转 VNode + +```typescript +import { convertNodeListToVNodes } from '../_util/component'; + +// 将 DOM NodeList 转换为 VNode(带缓存优化) +const vnodes = convertNodeListToVNodes(element.childNodes); +``` + +### getChildrenArray - children 转数组 + +```typescript +import { getChildrenArray } from '../_util/component'; + +const childrenArray = getChildrenArray(this.props.children); +``` + +### setExportparts - 导出 Shadow DOM parts + +```typescript +import { setExportparts } from '../_util/component'; + +ready() { + setExportparts(this); // 允许外部样式穿透 Shadow DOM +} +``` + +--- + +## convertToLightDomNode - Light DOM 支持 + +将 Shadow DOM 组件转换为 Light DOM 模式,用于需要样式穿透的场景。 + +### 基本用法 + +```typescript +import { convertToLightDomNode } from '../_util/lightDom'; + +// 典型场景:Message、Notification 等需要挂载到 body 的组件 +render( + convertToLightDomNode( + 操作成功 + ), + container +); +``` + +### 工作原理 + +1. 继承原组件构建 `isLightDOM = true` 的新组件 +2. 在 `beforeRender` 中将样式表合并到父级 ShadowRoot 或 document +3. 注册为 `{tagName}-light-dom` 的新组件 + +### 使用场景 + +- 插件式组件(DialogPlugin、MessagePlugin、NotificationPlugin) +- 需要脱离当前 Shadow DOM 渲染的场景 +- Portal 类组件 + +--- + +## classname - 类名生成 + +```typescript +import classname, { getClassPrefix } from '../_util/classname'; + +const classPrefix = getClassPrefix(); // 't' +const cls = classname( + className, // 外部传入的 className + `${classPrefix}-button`, // 基础类名 + `${classPrefix}-button--theme-${theme}`, // 修饰符 + { + [`${classPrefix}-is-disabled`]: disabled, // 条件类名 + [`${classPrefix}-is-loading`]: loading, + } +); +``` + +--- + +## createEmit - 事件派发(fire 的封装) + +`emit` 是 Omi 原生 `fire` 的封装,自动检测环境:Omi 环境跳过派发,非 Omi 环境调用 `fire()`。 + +```typescript +private emit = createEmit(this); + +handleChange = (e: Event) => { + this.props.onChange?.(value, { e }); // Omi 环境 + this.emit('change', { value, e }); // 非 Omi 环境 +}; +``` + +**不要同时使用 `emit` 和 `fire`**。