Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions .codebuddy/skills/tdesign-webcomponents-dev/SKILL.md
Original file line number Diff line number Diff line change
@@ -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<XxxProps> {
static css = [];
static propTypes = { theme: String, size: String, disabled: Boolean };
static defaultProps: Partial<XxxProps> = { 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 (
<div
className={classname(props.className, `${classPrefix}-xxx`, {
[`${classPrefix}-is-disabled`]: props.disabled,
})}
style={props.style}
onClick={this.handleClick}
>
<slot>{props.children}</slot>
</div>
);
}
}
```

---

## 生命周期

| 生命周期 | 触发时机 | 用途 |
|---------|---------|------|
| `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(<t-message />), 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- 前缀 | `<t-button>` |

### 受控/非受控

| 受控 | 非受控 | 回调 |
|-----|-------|------|
| `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<ButtonProps>('t-button');

// React 中使用
<TButton theme="primary" onClick={handleClick}>按钮</TButton>
```

### Slot 声明

有自定义 slot 时需声明 `slotProps`:

```typescript
@tag('t-select')
export default class Select extends Component<SelectProps> {
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 版本一致
122 changes: 122 additions & 0 deletions .codebuddy/skills/tdesign-webcomponents-dev/references/lifecycle.md
Original file line number Diff line number Diff line change
@@ -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<XxxProps> {
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)
- 外部订阅
Loading
Loading