diff --git a/package.json b/package.json index bab53b8..74193c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne/example-driver", - "version": "0.1.17", + "version": "0.1.18", "description": "用于在线展示和编辑React组件", "syntax": { "esmodules": true @@ -16,11 +16,10 @@ "init-example": "modules-dev-libs-init", "build:md": "npx @kne/md-doc", "start:md": "npx @kne/md-doc --watch", - "build:types": "cp src/index.d.ts dist/", - "build:package-manifest": "cp src/package-manifest.json dist/", - "build:lib-main": "microbundle --no-compress --format modern,cjs --jsx React.createElement --jsxFragment React.Fragment", - "build:lib": "run-s build:lib-main build:types build:package-manifest", - "start:lib": "microbundle watch --no-compress --format modern,cjs --jsx React.createElement --jsxFragment React.Fragment", + "build:locale": "microbundle src/locale/*.js -o dist/locale --no-compress --format modern,cjs ", + "build:lib-main": "microbundle --no-compress --format modern,cjs --jsxImportSource react --jsx React.createElement --jsxFragment React.Fragment", + "build:lib": "run-s build:locale build:lib-main", + "start:lib": "microbundle watch --no-compress --format modern,cjs --jsxImportSource react --jsx React.createElement --jsxFragment React.Fragment", "build:example": "cd example && npm run build", "start:example": "cd example && npm run start", "test:build": "run-s build", @@ -72,14 +71,15 @@ "husky": "^9.0.11", "npm-run-all": "^4.1.5", "prettier": "^2.5.1", - "react-scripts": "^5.0.0", - "sass": "^1.97.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-scripts": "^5.0.0", + "sass": "^1.97.3" }, "dependencies": { "@babel/standalone": "^7.24.0", "@kne/react-error-boundary": "^0.1.1", + "@kne/react-intl": "^0.1.12", "@monaco-editor/react": "^4.6.0", "babel-standalone": "^6.26.0", "classnames": "^2.3.1", diff --git a/prompts/prompts-frontend-libs/README.md b/prompts/prompts-frontend-libs/README.md new file mode 100644 index 0000000..33fe5be --- /dev/null +++ b/prompts/prompts-frontend-libs/README.md @@ -0,0 +1,132 @@ +# Prompts Frontend Libs 文档索引 + +本项目包含多个前端开发辅助提示词文档,用于指导 AI 完成常见的前端组件库开发任务。 + +## 文档列表 + +### 1. 组件国际化 + +**功能**: 指导将 React 组件完成国际化,支持多语言切换 + +**适用场景**: +- 组件需要支持多语言显示 +- 项目需要面向国际用户 +- 现有组件需要添加国际化支持 + +**核心内容**: +- useIntl Hook 使用方法 +- withLocale HOC 包裹模式 +- 语言包配置规范 +- 不同组件类型的国际化处理方式 + +**使用方式**: 阅读 [国际化](prompts/国际化.md) 了解详情 + +--- + +### 2. 添加 TypeScript 类型声明 + +**功能**: 为现有的 React 组件库添加完整的 TypeScript 类型定义 + +**适用场景**: +- JavaScript 组件库需要添加 TypeScript 支持 +- 提升代码可维护性和 IDE 智能提示 +- 便于团队协作和代码审查 + +**核心内容**: +- Props 接口定义规范 +- 类型声明文件结构 +- 复杂场景处理(嵌套对象、函数属性、联合类型) +- 组件导出声明模板 + +**使用方式**: 阅读 [添加ts类型声明](prompts/添加ts类型声明.md) 了解详情 + +--- + +### 3. 完善 package.json 描述和关键词 + +**功能**: 为项目完善 package.json 的 description 和 keywords 字段 + +**适用场景**: +- 新建项目需要完善 package.json 信息 +- 项目功能更新后需要调整描述 +- 提升 npm 包的搜索可发现性 + +**核心内容**: +- 描述撰写原则和模板 +- 关键词分类和提取来源 +- 信息收集和分析流程 +- 输出格式和检查清单 + +**使用方式**: 阅读 [完善package.json描述和关键词](prompts/完善package.json描述和关键词.md) 了解详情 + +--- + +### 4. 生成包功能描述文件 + +**功能**: 为 JavaScript/TypeScript 包生成 package-manifest.json 描述文件 + +**适用场景**: +- 自动化 API 文档生成 +- IDE 智能提示和自动补全 +- 包管理和分发平台的组件展示 +- 第三方工具的集成和分析 + +**核心内容**: +- 项目结构分析流程 +- 导出模块识别方法 +- JSON 结构构建规范 +- 类型定义标准和描述编写原则 + +**使用方式**: 阅读 [生成包功能描述文件](prompts/生成包功能描述文件.md) 了解详情 + +--- + +### 5. 生成文档 + +**功能**: 为项目生成标准化的 API 文档和概述文档 + +**适用场景**: +- 需要生成项目概述文档 (summary.md) +- 需要生成 API 文档 (api.md) +- 规范化项目文档结构 + +**核心内容**: +- 文档结构规范 +- 标题级别使用规范 +- 表格格式要求 +- 内容基于实际代码分析的原则 + +**使用方式**: 阅读 [生成文档](prompts/生成文档.md) 了解详情 + +--- + +### 6. 组件示例编写 + +**功能**: 编写遵循特定目录结构规范的 React 组件示例 + +**适用场景**: +- 为组件创建可运行的示例代码 +- 编写组件演示和文档 +- 构建组件库的示例展示系统 + +**核心内容**: +- 项目目录布局规范 +- example.json 配置结构 +- 示例代码编写模板 +- scope 依赖声明规则 +- 数据模拟方法 + +**使用方式**: 阅读 [组件示例编写](prompts/组件示例编写.md) 了解详情 + +--- + +## 如何选择 + +| 需求 | 使用文档 | +|------|----------| +| 组件需要支持多语言 | 组件国际化 | +| 为 JS 组件库添加 TS 类型支持 | 添加 TypeScript 类型声明 | +| 完善 package.json 描述和关键词 | 完善 package.json 描述和关键词 | +| 生成包的 API 描述文件 | 生成包功能描述文件 | +| 生成项目概述和 API 文档 | 生成文档 | +| 编写组件演示示例代码 | 组件示例编写 | diff --git "a/prompts/prompts-frontend-libs/\345\233\275\351\231\205\345\214\226.md" "b/prompts/prompts-frontend-libs/\345\233\275\351\231\205\345\214\226.md" new file mode 100644 index 0000000..9ec750e --- /dev/null +++ "b/prompts/prompts-frontend-libs/\345\233\275\351\231\205\345\214\226.md" @@ -0,0 +1,217 @@ +# 组件国际化指南 + +## 概述 + +本指南用于将组件完成国际化。 + +## 一、创建的文件 + +1. **`src/withLocale.js`** - 统一的国际化 HOC,所有组件共用 +2. **`src/locale/zh-CN.js`** - 中文语言包,所有组件共用 +3. **`src/locale/en-US.js`** - 英文语言包,所有组件共用 + +## 二、需要修改的文件类型 + +### 主组件文件 +- 添加 `useIntl` Hook +- 用 `withLocale` 包裹导出 + +### FormInner 表单组件 +- 添加 `useIntl` Hook +- 用 `withLocale` 包裹导出 +- 表单 label 国际化 + +### getColumns 等工具函数 +- 通过参数接收 `formatMessage` +- 移除内部的 `useIntl` 和 `withLocale` 引入 + +### Action 操作组件 +- 添加 `useIntl` Hook +- 用 `withLocale` 包裹导出 + +## 三、国际化的关键模式 + +### 1. useIntl Hook 使用 +```javascript +import { useIntl } from '@kne/react-intl'; + +const Component = () => { + const { formatMessage } = useIntl(); + return
{formatMessage({ id: 'ComponentName.Key' })}
; +}; +``` + +### 2. withLocale 包裹普通组件 +```javascript +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; + +const ComponentInner = ({ ...props }) => { + const { formatMessage } = useIntl(); + // 将所有中文替换为 formatMessage({ id: 'ComponentName.Key' }) + return
{formatMessage({ id: 'ComponentName.Key' })}
; +}; + +const Component = withLocale(ComponentInner); + +export default Component; +``` + +### 3. createWithRemoteLoader 组件(推荐格式) +```javascript +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; + +const ComponentInner = createWithRemoteLoader({...})(({ remoteModules, ...props }) => { + const { formatMessage } = useIntl(); + // ... +}); + +const Component = withLocale(ComponentInner); + +export default Component; +``` + +**注意:** 对于 `createWithRemoteLoader` 创建的组件,可以先用 `ComponentInner` 存储,再用 `withLocale` 包裹。 + +### 4. getColumns 等工具函数(formatMessage 从父组件传入) +```javascript +const getColumns = ({formatMessage}) => { + return [ + { + name: 'name', + title: formatMessage({ id: 'ComponentName.Key' }) + } + ]; +}; + +// 父组件中使用 +const columns = getColumns({formatMessage}); +``` + +### 5. 带参数的翻译 +```javascript +formatMessage({ id: 'ComponentName.KeyWithParam' }, { name: value }) +``` + +## 四、注意事项 + +1. **所有使用 `useIntl` 的组件必须用 `withLocale` 包裹** +2. **`getColumns` 等工具函数通过参数接收 `formatMessage`,不使用 `useIntl`** +3. **语言包中避免重复的 key**,命名规则:`组件名 + 功能名`,如 `PersonalCard.status.online` +4. **`createWithRemoteLoader` 创建的组件内部使用 useIntl 时,外层需要重命名并用 withLocale 包裹** +5. 注意检查 `withLocale`文件的引用地址,根目录组件使用 `../withLocale` + +--- + +# 组件国际化操作步骤 + +## 步骤 + +### 1. 更新语言包文件 +在 `src/locale/zh-CN.js` 和 `src/locale/en-US.js` 中添加对应的翻译文本: +```javascript +// zh-CN.js +const locale = { + ComponentName: { + status: { + online: '在线', + offline: '离线' + }, + summary: '简介' + } +}; + +// en-US.js +const locale = { + ComponentName: { + status: { + online: 'Online', + offline: 'Offline' + }, + summary: 'Summary' + } +}; +``` + +### 2. 修改组件文件 + +#### 主组件修改模式: +```javascript +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; + +const ComponentInner = ({ ...props }) => { + const { formatMessage } = useIntl(); + // 将所有中文替换为 formatMessage({ id: 'ComponentName.Key' }) + return ( + // ... + ); +}; + +const Component = withLocale(ComponentInner); + +export default Component; +``` + +#### FormInner 修改模式: +```javascript +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; + +const FormInnerInner = createWithRemoteLoader({...})(({ remoteModules, ...props }) => { + const { formatMessage } = useIntl(); + // label={formatMessage({ id: 'ComponentName.Key' })} + // ... +}); + +const FormInner = withLocale(FormInnerInner); + +export default FormInner; +``` + +#### Action 操作组件修改模式: +```javascript +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; + +const ActionInner = createWithRemoteLoader({...})(({ remoteModules, ...props }) => { + const { formatMessage } = useIntl(); + // ... +}); + +const ActionComponent = withLocale(ActionInner); + +export default ActionComponent; +``` + +#### getColumns 修改模式: +```javascript +// 移除 useIntl 和 withLocale 引入 +const getColumns = ({formatMessage}) => { + return [ + { + name: 'xxx', + title: formatMessage({ id: 'ComponentName.Key' }) + } + ]; +}; +``` + +父组件中调用:`getColumns({formatMessage})` + +### 3. 语言包 key 命名规范 +- 避免重复,使用 `组件名+功能名` 格式,如 `PersonalCard.status.online`、`UserName`、`UserRole`、`SettingType` +- 中文和英文语言包保持完全一致的 key 结构 + +### 4. 检查要点 +- [ ] 所有使用 `useIntl` 的组件都用 `withLocale` 包裹 +- [ ] `getColumns` 等工具函数通过参数接收 `formatMessage` +- [ ] 语言包中无重复 key +- [ ] `withLocale` 引用路径正确(组件在子目录使用 `../withLocale`) + +### 5. 最后检查 +运行命令找到所有使用 useIntl 的文件,确保都已正确包裹: +```bash +grep -r "useIntl" src/ --include="*.js" -l +``` diff --git "a/prompts/prompts-frontend-libs/\345\256\214\345\226\204package.json\346\217\217\350\277\260\345\222\214\345\205\263\351\224\256\350\257\215.md" "b/prompts/prompts-frontend-libs/\345\256\214\345\226\204package.json\346\217\217\350\277\260\345\222\214\345\205\263\351\224\256\350\257\215.md" new file mode 100644 index 0000000..b6438cc --- /dev/null +++ "b/prompts/prompts-frontend-libs/\345\256\214\345\226\204package.json\346\217\217\350\277\260\345\222\214\345\205\263\351\224\256\350\257\215.md" @@ -0,0 +1,142 @@ +# 完善 package.json 描述和关键词 + +## 任务描述 + +为 JavaScript/TypeScript 包项目的 `package.json` 文件完善 `description` 和 `keywords` 字段,使其准确反映项目的功能定位和核心特性。 + +## 实施步骤 + +### 1. 收集信息 + +#### 1.1 分析文档目录 +- 阅读 `doc/api.md` 了解组件 API 和功能列表 +- 阅读 `README.md` 了解项目概述和主要特性 +- 查看 `doc/example.json` 了解示例配置 + +#### 1.2 分析源码结构 +- 查看 `src/` 目录结构,识别导出的组件和模块 +- 分析主入口文件 `src/index.js` 的导出内容 +- 检查组件的 props 和功能特性 + +#### 1.3 分析依赖关系 +- 查看 `package.json` 中的 `peerDependencies` 了解技术栈 +- 查看 `dependencies` 了解核心依赖库 + +### 2. 撰写 description + +#### 2.1 描述结构 +``` +[核心定位] + [主要功能] + [适用场景] +``` + +#### 2.2 撰写原则 +- **简洁明了**:控制在 1-2 句话,不超过 100 个字符 +- **突出特色**:强调项目的独特价值 +- **用户视角**:从使用者角度描述功能 +- **避免技术细节**:不包含实现细节 + +#### 2.3 描述模板 + +**React 组件库模板**: +``` +一个用于 [场景] 的 React 组件库,提供 [核心组件/功能],支持 [特性列表]。 +``` + +**工具库模板**: +``` +用于 [场景] 的 [类型] 工具库,提供 [核心功能],支持 [特性列表]。 +``` + +### 3. 生成 keywords + +#### 3.1 关键词分类 + +| 分类 | 示例 | 说明 | +|------|------|------| +| 核心功能 | `select`, `picker`, `dropdown` | 描述主要功能 | +| 技术栈 | `react`, `antd`, `typescript` | 依赖的技术框架 | +| 组件类型 | `component`, `ui`, `form` | 组件分类 | +| 数据结构 | `tree`, `table`, `list` | 支持的数据展示形式 | +| 特性 | `multiselect`, `searchable`, `i18n` | 核心特性标签 | +| 场景 | `enterprise`, `admin`, `dashboard` | 适用场景 | + +#### 3.2 关键词数量 +- 建议数量:5-15 个 +- 按重要性排序 +- 包含英文和常见缩写 + +#### 3.3 关键词提取来源 + +1. **组件名称**:从导出的组件名称提取核心词汇 +2. **功能特性**:从支持的功能特性提取标签 +3. **技术依赖**:从 peerDependencies 和 dependencies 提取技术栈 +4. **使用场景**:从项目定位提取适用场景 + +### 4. 输出格式 + +#### 4.1 JSON 格式 +```json +{ + "description": "完善的描述内容", + "keywords": [ + "keyword1", + "keyword2", + "keyword3" + ] +} +``` + +#### 4.2 直接修改 package.json +将生成的内容更新到 `package.json` 的对应字段。 + +## 示例 + +### 示例分析流程 + +**步骤一:源码分析** +- 识别 `src/index.js` 导出的所有模块和组件 +- 分析组件的 props、功能特性 +- 确定技术栈(如 React、Vue、TypeScript 等) + +**步骤二:文档分析** +- 阅读 `README.md` 获取项目概述 +- 分析 `doc/api.md` 了解 API 功能列表 +- 查看示例代码理解使用场景 + +**步骤三:生成结果** +- 根据 分析结果撰写 description +- 从组件功能、技术栈、特性等维度提取 keywords +- 输出 JSON 格式的 description 和 keywords + +**示例输出格式**: +```json +{ + "description": "一个用于 [场景] 的 React 组件库,提供 [核心功能],支持 [特性列表]", + "keywords": [ + "react", + "component", + "功能关键词", + "技术栈关键词", + "特性关键词" + ] +} +``` + +## 检查清单 + +- [ ] description 长度适中(不超过 100 字符) +- [ ] description 包含核心功能和特色 +- [ ] keywords 包含技术栈标签 +- [ ] keywords 包含功能标签 +- [ ] keywords 按重要性排序 +- [ ] 没有重复或冗余的关键词 +- [ ] 关键词使用小写字母 +- [ ] 关键词使用连字符分隔单词(如 `tree-select`) + +## 注意事项 + +1. **避免过度宣传**:描述应客观准确,不使用夸张词汇 +2. **保持一致性**:关键词应与项目实际功能一致 +3. **考虑搜索优化**:使用 npm 搜索中常见的关键词 +4. **参考同类项目**:可参考 npm 上类似项目的关键词设置 +5. **定期更新**:当项目功能变化时,同步更新描述和关键词 diff --git "a/prompts/prompts-frontend-libs/\346\267\273\345\212\240ts\347\261\273\345\236\213\345\243\260\346\230\216.md" "b/prompts/prompts-frontend-libs/\346\267\273\345\212\240ts\347\261\273\345\236\213\345\243\260\346\230\216.md" new file mode 100644 index 0000000..f6117f6 --- /dev/null +++ "b/prompts/prompts-frontend-libs/\346\267\273\345\212\240ts\347\261\273\345\236\213\345\243\260\346\230\216.md" @@ -0,0 +1,103 @@ +# 为React组件库添加TypeScript类型定义的通用提示词 + +## 任务描述 + +为一个已有的React组件库添加完整的TypeScript类型定义,包括所有组件的Props接口、类型声明和导出。 + +## 实施步骤 + +### 1. 创建类型定义文件 + +在源代码目录创建`index.d.ts`文件,位置与主入口文件`index.js`同级。 + +### 2. 分析组件结构 + +- 查看所有组件文件的Props参数 +- 确定每个组件的输入属性类型 +- 识别回调函数的参数和返回值类型 + +### 3. 定义基础类型 + +```typescript +import { ReactNode, ComponentType, FC } from 'react'; +``` + +### 4. 为每个组件创建Props接口 + +- 组件名 + Props 作为接口名(如:FormInfoProps) +- 所有可选属性使用`?`标记 +- ReactNode类型用于支持字符串和JSX元素 +- 函数类型明确定义参数和返回值 +- 使用`[key: string]: any`支持动态属性 + +### 5. 处理复杂场景 + +- **嵌套对象属性**:定义内联对象类型 +- **函数属性**:明确参数类型和返回值类型 +- **联合类型**:使用`|`支持多种类型 +- **泛型组件**:使用ComponentType + +### 6. 导出组件类型声明 + +```typescript +export declare const ComponentName: FC; +``` + +## 类型定义模板 + +### 基础组件Props模板 + +```typescript +export interface ComponentNameProps { + className?: string; + children?: ReactNode; + + // 其他属性... + [key: string]: any; // 支持动态属性 +} +``` + +### 函数回调模板 + +```typescript +onEvent ? : (data: any, context: any, ...args: any[]) => Promise | any; +``` + +### 组件声明模板 + +```typescript +export declare const ComponentName: FC; +``` + +## 最佳实践 + +1. **类型完整性**:覆盖所有组件的公开API +2. **向后兼容**:保持现有JavaScript代码的正常运行 +3. **可选属性**:合理使用可选属性,提供良好的开发体验 +4. **文档注释**:为复杂类型添加JSDoc注释 +5. **版本控制**:在package.json中指定types字段指向类型文件 + +## 使用场景 + +- 为现有组件库添加TypeScript支持 +- 提升代码可维护性和开发体验 +- 支持IDE智能提示和类型检查 +- 便于团队协作和代码审查 + +## 示例项目结构 + +``` +src/ +├── index.js # 主入口文件 +├── index.d.ts # 类型定义文件(新增) +├── ComponentA.js # 组件A +├── ComponentB.js # 组件B +└── ... +``` + +## 注意事项 + +- 类型定义文件应与实际组件实现保持同步 +- 考虑使用更具体的类型替代`any`类型 +- 定期检查和更新类型定义以匹配组件API的变化 +- 对于复杂的组件,考虑拆分类型定义到多个文件中 \ No newline at end of file diff --git "a/prompts/prompts-frontend-libs/\347\224\237\346\210\220\345\214\205\345\212\237\350\203\275\346\217\217\350\277\260\346\226\207\344\273\266.md" "b/prompts/prompts-frontend-libs/\347\224\237\346\210\220\345\214\205\345\212\237\350\203\275\346\217\217\350\277\260\346\226\207\344\273\266.md" new file mode 100644 index 0000000..39dfcd3 --- /dev/null +++ "b/prompts/prompts-frontend-libs/\347\224\237\346\210\220\345\214\205\345\212\237\350\203\275\346\217\217\350\277\260\346\226\207\344\273\266.md" @@ -0,0 +1,179 @@ +# 生成包功能描述文件(package-manifest.json)的通用提示词 + +## 任务描述 + +为一个JavaScript/TypeScript包项目生成`package-manifest.json`文件,该文件用于描述包的主要功能、导出模块、组件API等详细信息。 + +## 实施步骤 + +### 1. 分析项目结构 + +- 查看`package.json`了解包的基本信息 +- 分析主入口文件(`index.js`/`index.ts`)的导出内容 +- 检查源代码目录结构,识别所有可导出的模块/组件 + +### 2. 识别导出模块 + +- 默认导出:通常标记为`default` +- 命名导出:所有具名导出的组件、函数、类等 + +### 3. 分析每个模块的功能 + +对于每个导出的模块,需要收集以下信息: + +#### React组件 +- **description**: 组件的主要功能描述 +- **type**: 通常为`ReactNode` +- **props**: 组件的所有属性,包括: + - 属性名 + - 属性描述 + - 属性类型(支持联合类型,如`string|number`,不能使用下面`类型定义规范`定义之外的类型) + +#### 函数/类 +- **description**: 功能描述 +- **type**: 返回值类型 +- **parameters**: 参数列表(如果有) +- **properties**: 类属性(如果是类) + +#### 常量/对象 +- **description**: 用途描述 +- **type**: 数据类型 +- **properties**: 对象属性结构 + +### 4. 构建JSON结构 + +```json +{ + "description": "包的主要导出模块入口", + "modules": { + "模块名": { + "description": "模块功能描述", + "type": "类型声明", + // 根据模块类型可能包含以下字段 + "props": {}, // React组件属性 + "parameters": [], // 函数参数 + "properties": {}, // 对象/类属性 + "returns": "返回值类型" // 函数返回值 + } + } +} +``` + +## 类型定义规范 + +### 基础类型 +- `string`: 字符串 +- `number`: 数字 +- `boolean`: 布尔值 +- `object`: 对象 +- `array`: 数组 +- `function`: 函数 +- `Date`: 日期对象 +- `Promise`: Promise对象 +- `RegExp`: 正则表达式 +- `Error`: 错误对象 +- `undefined`: 未定义 +- `null`: 空值 +- `any`: 任意类型 +- `void`: 无返回值 +- `unknown`: 未知类型 +- `ReactNode`: React节点(组件专用) + +### 联合类型 +使用`|`连接多种类型: +- `string|ReactNode`: 字符串或React节点 +- `number|object`: 数字或对象 +- `function|null`: 函数或null + +### 复杂类型 +- `object[]`: 对象数组 +- `ReactNode[]`: React节点数组 +- `string[]`: 字符串数组 + +## 优先级排序 + +1. **主要组件**: 包中最核心、最常用的组件 +2. **辅助组件**: 支持性组件 +3. **工具函数**: 纯函数、工具方法 +4. **类型定义**: TypeScript类型、接口 +5. **常量配置**: 配置对象、常量 + +## 属性命名约定 + +### 通用属性 +- `className`: CSS类名 +- `children`: 子元素 +- `style`: 样式对象 +- `title`: 标题 +- `description`: 描述 + +## 描述编写原则 + +1. **简洁明确**: 一句话说明核心功能 +2. **用户视角**: 从使用者的角度描述 +3. **突出价值**: 说明解决了什么问题 +4. **避免实现细节**: 不描述内部实现逻辑 + +## 示例格式 + +### React组件示例 +```json +{ + "FormModal": { + "description": "模态框表单组件,在弹窗中展示表单", + "type": "ReactNode", + "props": { + "open": { + "description": "是否打开模态框", + "type": "boolean" + }, + "title": { + "description": "模态框标题", + "type": "ReactNode" + }, + "onSubmit": { + "description": "提交回调函数", + "type": "function" + } + } + } +} +``` + +### 工具函数示例 +```json +{ + "formatDate": { + "description": "格式化日期字符串", + "type": "string", + "parameters": [ + { + "name": "format", + "description": "格式化模板", + "type": "string" + } + ] + } +} +``` + +## 文件位置 + +将生成的`package-manifest.json`文件放置在: +- `src/package-manifest.json` + +## 使用场景 + +- 自动化API文档生成 +- IDE智能提示和自动补全 +- 包管理和分发平台的组件展示 +- 团队协作时的API参考 +- 第三方工具的集成和分析 + +## 注意事项 + +- 避免包含内部实现细节 +- 不要包含私有方法和属性 +- 确保向后兼容性 +- 定期更新以匹配代码变更 +- 考虑国际化需求(如需要) \ No newline at end of file diff --git "a/prompts/prompts-frontend-libs/\347\224\237\346\210\220\346\226\207\346\241\243.md" "b/prompts/prompts-frontend-libs/\347\224\237\346\210\220\346\226\207\346\241\243.md" new file mode 100644 index 0000000..3476ca7 --- /dev/null +++ "b/prompts/prompts-frontend-libs/\347\224\237\346\210\220\346\226\207\346\241\243.md" @@ -0,0 +1,20 @@ +### 文档生成说明 + +本次文档生成遵循以下规范: + +#### 文档结构 + +1. **doc/summary.md** - 项目概述文档 + - 使用 h3 及以下标题级别 + - 包含:项目概述、主要特性、使用场景 + +2. **doc/api.md** - API 文档 + - 使用 h3 及以下标题级别 + - 使用表格格式展示命令和参数 + - 包含:命令行接口、程序化 API、输出结构说明 + +#### 文档要求 + +- 标题级别:仅使用 h3(###)及以下 +- 表格格式:属性名、类型/参数、默认值/返回值、说明 +- 内容基于实际代码分析,确保准确性 diff --git "a/prompts/prompts-frontend-libs/\347\273\204\344\273\266\347\244\272\344\276\213\347\274\226\345\206\231.md" "b/prompts/prompts-frontend-libs/\347\273\204\344\273\266\347\244\272\344\276\213\347\274\226\345\206\231.md" new file mode 100644 index 0000000..df906c5 --- /dev/null +++ "b/prompts/prompts-frontend-libs/\347\273\204\344\273\266\347\244\272\344\276\213\347\274\226\345\206\231.md" @@ -0,0 +1,316 @@ +# 组件示例编写提示词 + +本提示词用于编写遵循特定目录结构规范的 React 项目组件示例。 + +## 1. 目录结构规范 + +### 项目目录布局 + +``` +{PROJECT_ROOT}/ +├── src/ # 源代码目录 +│ └── components/ # 组件源代码 +│ ├── ComponentA/ # 单个组件目录 +│ ├── ComponentB/ # 带子组件的目录 +│ │ ├── Sub1/ +│ │ ├── Sub2/ +│ │ └── index.js +│ └── ... +├── doc/ # 文档及示例目录(所有组件共用) +│ ├── example.json # 示例配置文件(核心) +│ ├── api.md # 组件API文档 +│ ├── summary.md # 组件简要描述 +│ ├── base.js # 基础示例代码 +│ ├── component-a.js # ComponentA示例 +│ ├── component-b.js # ComponentB主组件示例 +│ ├── component-b-sub1.js # ComponentB子组件Sub1示例 +│ ├── component-b-sub2.js # ComponentB子组件Sub2示例 +│ └── style.scss # 示例样式(可选) +└── package.json # 项目配置文件 +``` + +### 示例文件规则 + +**重要**:所有组件的示例文件都统一放在项目根目录的 `doc/` 中,而不是在每个组件目录下创建单独的 `doc/` 目录。 + +- 单个组件:使用组件名的小写短横线形式命名示例文件,如 `component-a.js` +- 带子组件的组件:主组件示例使用组件名,子组件示例使用组件名加子组件名,如 `component-b.js`、`component-b-sub1.js` + +### 项目配置变量 + +在编写示例时需要替换以下变量: + +| 变量名 | 说明 | 示例值 | +|--------|------|--------| +| `{PACKAGE_NAME}` | 从 package.json 的 name 字段获取 | `@kne/react-file` | +| `{PROJECT_NAME}` | 项目名称,用于 getPublicPath | `react-file` | +| `{LIB_NAME}` | 组件库名称,通常是驼峰形式 | `ReactFile` | + +## 2. example.json 配置结构 + +所有组件的示例配置统一在 `doc/example.json` 中管理: + +```json +{ + "isFull": true, + "list": [ + { + "title": "ComponentA", + "description": "组件A的描述", + "code": "./component-a.js", + "scope": [ + { + "name": "_{LIB_NAME}", + "packageName": "{PACKAGE_NAME}", + "importStatement": "import * as _{LIB_NAME} from \"{PACKAGE_NAME}\"" + }, + { + "packageName": "{PACKAGE_NAME}/dist/index.css" + }, + { + "name": "antd", + "packageName": "antd" + }, + { + "name": "remoteLoader", + "packageName": "@kne/remote-loader" + } + ] + }, + { + "title": "ComponentB", + "description": "组件B主组件", + "code": "./component-b.js", + "scope": [] + }, + { + "title": "ComponentB-Sub1", + "description": "组件B子组件Sub1", + "code": "./component-b-sub1.js", + "scope": [] + } + ] +} +``` + +**重要说明**: +- `packageName` 可以使用当前组件包名的特殊格式:`@kne/current-lib_{PACKAGE_NAME_SUFFIX}` +- 例如:`@kne/react-file` 项目可以使用 `@kne/current-lib_react-file` 来引用当前正在开发的组件包 +- 这种特殊写法用于文档示例系统自动解析当前包名 + +## 3. 示例代码规范 + +### 示例编写原则 + +**重要原则**: + +1. **充分展示API功能**:示例代码要尽可能完整地展示组件的所有API功能和配置选项,包括: + - 组件的所有核心属性(props)及其不同取值 + - 事件回调函数的完整实现 + - 不同配置模式下的使用场景 + - 组件间的组合使用方式 + - 各种边界情况和特殊场景 + +2. **贴近业务场景的示例数据**:示例数据应尽量模拟真实的业务场景,避免过于简化的假数据: + - 使用真实的字段名称和数据结构 + - 包含合理的业务上下文信息 + - 体现实际应用中的复杂度和多样性 + - 模拟真实接口返回的数据格式 + +### 导入方式 + +- 使用 `scope` 中声明的变量名:`const { FilePreview } = _{LIB_NAME};` +- 工具包引用:`const { createWithRemoteLoader, getPublicPath } = remoteLoader;` + +### 标准示例模板(需要 PureGlobal 模拟数据) + +```javascript +const { YourComponent } = _{LIB_NAME}; +const { createWithRemoteLoader, getPublicPath } = remoteLoader; + +const BaseExample = createWithRemoteLoader({ + modules: ['components-core:Global@PureGlobal', 'components-core:InfoPage'] +})(({ remoteModules }) => { + const [PureGlobal, InfoPage] = remoteModules; + return ( + { + return { data: { code: 0, data: api.loader() } }; + }, + apis: { + file: { + staticUrl: getPublicPath('{PROJECT_NAME}') || window.PUBLIC_URL, + getUrl: { + loader: async ({ params }) => { + // 模拟数据 + return 'mock-url'; + } + } + } + } + }}> + + + + + + + ); +}); + +render(); +``` + +### 简化示例模板(无需远程加载) + +```javascript +const { YourComponent } = _{LIB_NAME}; +const { Flex, Switch } = antd; +const { useState } = React; + +const BaseExample = () => { + const [state, setState] = useState(false); + return ( + + + + ); +}; + +render(); +``` + +## 4. scope 依赖声明规则 + +| 场景 | name | packageName | importStatement(可选) | +|------|------|-------------|------------------------| +| 项目组件 | `_{LIB_NAME}` | `{PACKAGE_NAME}` 或 `@kne/current-lib_{PACKAGE_NAME_SUFFIX}` | `import * as _{LIB_NAME} from "{PACKAGE_NAME}"` | +| 样式文件 | - | `{PACKAGE_NAME}/dist/index.css` 或 `@kne/current-lib_{PACKAGE_NAME_SUFFIX}/dist/index.css` | - | +| 远程加载器 | `remoteLoader` | `@kne/remote-loader` | - | +| Antd组件 | `antd` | `antd` | - | +| React原生 | `React` | - | -(无需声明) | + +**当前包引用规则**: +- 在 `example.json` 的 `scope` 中引用当前正在开发的组件包时,使用 `@kne/current-lib_{PACKAGE_NAME_SUFFIX}` +- 例如:`@kne/info-page` 项目应使用 `@kne/current-lib_info-page` +- 这样可以让文档系统自动识别并加载当前开发中的组件版本 + +## 5. API文档编写 + +`doc/api.md` 中包含所有组件的API文档,使用表格格式: + +```markdown +### 组件名称 + +组件描述 + +#### 属性 + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|-------|------| +| propName | string | - | 属性描述 | +``` + +## 6. 数据模拟 + +在示例中使用 PureGlobal 的 preset 模拟数据: + +```javascript +apis: { + file: { + staticUrl: getPublicPath('{PROJECT_NAME}') || window.PUBLIC_URL, + getUrl: { + loader: async ({ params }) => { + // 根据 params 返回对应的 mock 数据 + const urlMap = { + 1: '/mock/example.png', + 2: '/mock/example.pdf' + }; + return new Promise(resolve => { + setTimeout(() => { + resolve(urlMap[params.id]); + }, 500); + }); + } + } + } +} +``` + +## 7. 组件导出规范 + +在组件的主入口 `index.js` 中,需要导出所有子组件: + +```javascript +export { default } from './MainComponent'; +export { default as SubComponent1 } from './SubComponent1'; +export { default as SubComponent2 } from './SubComponent2'; +``` + +在示例中通过解构引用: + +```javascript +const { MainComponent, SubComponent1, SubComponent2 } = _{LIB_NAME}; +``` + +## 8. 使用说明 + +### 创建新组件示例步骤 + +1. 在 `src/components/` 下创建或编辑组件 +2. 在 `doc/example.json` 中添加示例配置 +3. 在 `doc/` 目录下创建对应的 `.js` 示例文件 +4. 在 `doc/api.md` 中添加组件API文档(如果是新组件) +5. 在组件的 `index.js` 中确保正确导出 + +### 变量替换示例 + +假设项目 `package.json` 中配置为: + +```json +{ + "name": "@your-org/your-project" +} +``` + +则变量值为: + +- `{PACKAGE_NAME}` → `@your-org/your-project` +- `{PROJECT_NAME}` → `your-project` +- `{LIB_NAME}` → `YourProject` +- `{PACKAGE_NAME_SUFFIX}` → `your-project`(用于当前包引用) + +实际使用时需要替换为: + +```json +{ + "name": "_YourProject", + "packageName": "@your-org/your-project", + "importStatement": "import * as _YourProject from \"@your-org/your-project\"" +} +``` + +**引用当前开发包时的特殊写法**: +```json +{ + "name": "_YourProject", + "packageName": "@kne/current-lib_your-project", + "importStatement": "import * as _YourProject from \"@your-org/your-project\"" +} +``` + +```javascript +const { YourComponent } = _YourProject; +const { createWithRemoteLoader, getPublicPath } = remoteLoader; + +// ... +staticUrl: getPublicPath('your-project') || window.PUBLIC_URL, +``` + +## 9. 项目参考 + +本项目的示例文件位于 `doc/` 目录: + +- `doc/example.json` - 所有组件的示例配置 +- `doc/api.md` - 所有组件的API文档 +- `doc/summary.md` - 项目概述和核心特性说明 \ No newline at end of file diff --git a/prompts/prompts.json b/prompts/prompts.json new file mode 100644 index 0000000..3d07349 --- /dev/null +++ b/prompts/prompts.json @@ -0,0 +1,3 @@ +{ + "@kne/prompts-frontend-libs": "1.0.1" +} diff --git "a/prompts/\347\224\237\346\210\220README\347\264\242\345\274\225.md" "b/prompts/\347\224\237\346\210\220README\347\264\242\345\274\225.md" new file mode 100644 index 0000000..b7f99c7 --- /dev/null +++ "b/prompts/\347\224\237\346\210\220README\347\264\242\345\274\225.md" @@ -0,0 +1,65 @@ +### 文档索引生成指南 + +#### 概述 + +本文档描述如何从包含多个子目录的文档目录生成结构清晰、便于查阅的索引文档。 + +#### 生成流程 + +##### 第一步:扫描源目录 + +识别当前文件所在目录中的所有子目录和文档文件中的所有README.md文件。 + +##### 第二步:分析子目录结构 + +对每个README.md进行内容分析: + +- 提取文档集合的核心用途和主要特性 +- 记录已安装的版本信息(如存在 prompts.json) + +##### 第三步:设计索引结构 + +采用统一的模板组织信息: + +```markdown +### 序号. 文档名称 + +**功能**: 简要描述 + +**适用场景**: +- 场景一 +- 场景二 + +**核心内容**: +- 内容点一 +- 内容点二 +``` + +##### 第四步:生成索引文档 + +按以下结构组织最终文档: + +1. **项目标题与简介**:说明文档集合的用途 +2. **文档集合列表**:按子目录分组展示各文档摘要 +3. **快速选择指南**:帮助用户根据需求快速定位 + +##### 第五步:添加导航辅助 + +在文末添加决策表格,帮助用户根据需求快速定位: + +| 需求 | 推荐文档 | 所属集合 | +|------|----------|----------| +| 具体需求描述 | 对应文档名称 | 所属子目录 | + +#### 设计原则 + +1. **信息层次清晰**:使用标题层级区分结构,使用加粗、列表等格式突出重点 +2. **内容适度抽象**:不直接复制原文内容,提炼核心要点 +3. **便于快速检索**:提供功能定位、明确适用场景、添加决策辅助表格 +4. **保持可维护性**:结构化模板便于更新,格式统一减少维护成本 + +#### 扩展建议 + +- **版本管理**:可在文档列表中添加版本号和更新日期 +- **标签系统**:为文档添加标签便于分类检索 +- **依赖关系**:说明文档之间的关联或前置依赖 diff --git a/src/components/CodePanel.js b/src/components/CodePanel.js new file mode 100644 index 0000000..d5edbd1 --- /dev/null +++ b/src/components/CodePanel.js @@ -0,0 +1,25 @@ +import React from 'react'; +import {useIntl} from '@kne/react-intl'; +import HighlightCode from './HighlightCode'; +import ErrorComponent from './ErrorComponent'; +import CodeEditor from '@monaco-editor/react'; + +const generateImportCode = (scope) => { + return scope.map(({name, packageName, importStatement}) => + importStatement ? importStatement : (name ? `import * as ${name} from '${packageName}';` : `import '${packageName}';`) + ).join('\n'); +}; + +const CodePanelInner = ({code, scope, error, editable, onChange}) => { + const {formatMessage} = useIntl(); + return (
+
+ +
+ + {error && } +
); +}; + +export default CodePanelInner; diff --git a/src/components/DescriptionBar.js b/src/components/DescriptionBar.js new file mode 100644 index 0000000..8e84110 --- /dev/null +++ b/src/components/DescriptionBar.js @@ -0,0 +1,13 @@ +import React from 'react'; + +const DescriptionBar = ({title, description, codeOpen, onToggle}) => { + return (
+ {title} +
+ + {codeOpen ? '' : '< >'} + +
); +}; + +export default DescriptionBar; diff --git a/src/components/DriverItem.js b/src/components/DriverItem.js new file mode 100644 index 0000000..b7ed723 --- /dev/null +++ b/src/components/DriverItem.js @@ -0,0 +1,18 @@ +import React from 'react'; +import classnames from 'classnames'; +import uniqueId from 'lodash/uniqueId'; +import LiveCode from './LiveCode'; +import MiniCode from './MiniCode'; + +const DriverItem = ({isFull, contextComponent, list}) => { + return
+ {list.map((props) => ( +
+ {props.hasOwnProperty('qrcodeUrl') ? : + } +
+ ))} +
; +}; + +export default DriverItem; diff --git a/src/components/ErrorComponent.js b/src/components/ErrorComponent.js new file mode 100644 index 0000000..054b5c5 --- /dev/null +++ b/src/components/ErrorComponent.js @@ -0,0 +1,9 @@ +import React, {memo} from 'react'; + +const ErrorComponent = memo(({error}) => { + return (
+ {error &&
{typeof error === 'string' ? error : error?.message}
} +
); +}); + +export default ErrorComponent; diff --git a/src/components/ExampleDriver.js b/src/components/ExampleDriver.js new file mode 100644 index 0000000..7f4d45d --- /dev/null +++ b/src/components/ExampleDriver.js @@ -0,0 +1,13 @@ +import React from 'react'; +import classnames from 'classnames'; +import DriverItem from './DriverItem'; + +const ExampleDriver = ({list, isFull, contextComponent, className, ...props}) => { + const groupList = isFull === true ? [list] : [list.filter((_, index) => index % 2 === 0), list.filter((_, index) => index % 2 !== 0)]; + return (
+ {groupList.map((item, index) => )} +
); +}; + +export default ExampleDriver; diff --git a/src/components/HighlightCode.js b/src/components/HighlightCode.js new file mode 100644 index 0000000..3a87fc5 --- /dev/null +++ b/src/components/HighlightCode.js @@ -0,0 +1,19 @@ +import React from 'react'; +import Highlight, {Prism} from "prism-react-renderer"; +import theme from '../theme'; + +const HighlightCode = ({code}) => { + return + {({tokens, getLineProps, getTokenProps}) => (<> + {tokens.map((line, i) => (
+ {line.map((token, key) => ())} +
))} + )} +
+}; + +export default HighlightCode; diff --git a/src/components/LiveCode.js b/src/components/LiveCode.js new file mode 100644 index 0000000..5833943 --- /dev/null +++ b/src/components/LiveCode.js @@ -0,0 +1,49 @@ +import React, {useEffect, useRef, useState, useMemo} from 'react'; +import ErrorBoundary from '@kne/react-error-boundary'; +import withLocale from '../withLocale'; +import {useInView, useLazyCompile, useReactRoot} from '../hooks'; +import DescriptionBar from './DescriptionBar'; +import CodePanel from './CodePanel'; +import ErrorComponent from './ErrorComponent'; + +const LiveCodeInner = ({code, scope, title, description, contextComponent}) => { + const [_code, setCode] = useState(code); + const [codeOpen, setCodeOpen] = useState(false); + const containerRef = useRef(null); + + const {shouldRender, heightRef} = useInView(containerRef); + const {compiledCode, error} = useLazyCompile(_code, shouldRender); + + const currentScope = useMemo(() => scope.filter(({component, name}) => !!component && !!name), [scope]); + + const [renderJsx, setRenderJsx] = useState(null); + + useEffect(() => { + if (!compiledCode || !shouldRender) return; + try { + // eslint-disable-next-line no-new-func + const runnerFunction = new Function('React', 'render', ...currentScope.map(({name}) => name), compiledCode); + const Component = contextComponent || (({children}) => children); + runnerFunction(React, jsx => setRenderJsx( + + {jsx} + + ), ...currentScope.map(({component}) => component)); + } catch (e) { + setRenderJsx(null); + } + }, [compiledCode, currentScope, contextComponent, shouldRender]); + + useReactRoot(containerRef, shouldRender, renderJsx, heightRef); + + return <> +
+ setCodeOpen(!codeOpen)}/> + {codeOpen && } + ; +}; + +const LiveCode = withLocale(LiveCodeInner); + +export default LiveCode; diff --git a/src/components/MiniCode.js b/src/components/MiniCode.js new file mode 100644 index 0000000..3f28fc3 --- /dev/null +++ b/src/components/MiniCode.js @@ -0,0 +1,25 @@ +import React, {useState} from 'react'; +import {useIntl} from '@kne/react-intl'; +import withLocale from '../withLocale'; +import DescriptionBar from './DescriptionBar'; +import CodePanel from './CodePanel'; + +const MiniCodeInner = ({code, qrcodeUrl, scope, title, description}) => { + const {formatMessage} = useIntl(); + const [codeOpen, setCodeOpen] = useState(false); + return <> +
+
+
{formatMessage({id:
+
{formatMessage({id: 'MiniCode.scanQrcode'})}
+
+
+ setCodeOpen(!codeOpen)}/> + {codeOpen && } + ; +}; + +const MiniCode = withLocale(MiniCodeInner); + +export default MiniCode; diff --git a/src/components/index.js b/src/components/index.js new file mode 100644 index 0000000..861263c --- /dev/null +++ b/src/components/index.js @@ -0,0 +1,8 @@ +export {default as ExampleDriver} from './ExampleDriver'; +export {default as DriverItem} from './DriverItem'; +export {default as LiveCode} from './LiveCode'; +export {default as MiniCode} from './MiniCode'; +export {default as CodePanel} from './CodePanel'; +export {default as DescriptionBar} from './DescriptionBar'; +export {default as HighlightCode} from './HighlightCode'; +export {default as ErrorComponent} from './ErrorComponent'; diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 0000000..6bb310a --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1,3 @@ +export {default as useLazyCompile} from './useLazyCompile'; +export {default as useInView} from './useInView'; +export {default as useReactRoot} from './useReactRoot'; diff --git a/src/hooks/useInView.js b/src/hooks/useInView.js new file mode 100644 index 0000000..cefeac7 --- /dev/null +++ b/src/hooks/useInView.js @@ -0,0 +1,33 @@ +import {useState, useEffect, useRef} from 'react'; + +const useInView = (ref) => { + const [shouldRender, setShouldRender] = useState(false); + const heightRef = useRef(0); + + useEffect(() => { + const container = ref.current; + if (!container) return; + + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setShouldRender(true); + } + if (!entry.isIntersecting && entry.intersectionRatio === 0) { + const h = container.getBoundingClientRect().height; + if (h > 0) { + heightRef.current = h; + } + setShouldRender(false); + } + }); + }, {threshold: [0]}); + + observer.observe(container); + return () => observer.disconnect(); + }, [ref]); + + return {shouldRender, heightRef}; +}; + +export default useInView; diff --git a/src/hooks/useLazyCompile.js b/src/hooks/useLazyCompile.js new file mode 100644 index 0000000..dac99e0 --- /dev/null +++ b/src/hooks/useLazyCompile.js @@ -0,0 +1,33 @@ +import {useState, useEffect} from 'react'; +import {transform as _transform} from '@babel/standalone'; +import {useDebouncedCallback} from 'use-debounce'; + +const useLazyCompile = (code, shouldCompile) => { + const [compiledCode, setCompiledCode] = useState(null); + const [error, setError] = useState(null); + + const compileCode = useDebouncedCallback((codeToCompile) => { + if (!codeToCompile) { + setCompiledCode(null); + return; + } + try { + setError(null); + const transformCode = _transform(codeToCompile, {presets: ['es2015', 'react']}).code; + setCompiledCode(transformCode); + } catch (e) { + setError(e); + setCompiledCode(null); + } + }, 500); + + useEffect(() => { + if (shouldCompile) { + compileCode(code); + } + }, [code, shouldCompile, compileCode]); + + return {compiledCode, error}; +}; + +export default useLazyCompile; diff --git a/src/hooks/useReactRoot.js b/src/hooks/useReactRoot.js new file mode 100644 index 0000000..b1bcf34 --- /dev/null +++ b/src/hooks/useReactRoot.js @@ -0,0 +1,50 @@ +import {useEffect, useRef} from 'react'; +import {createRoot} from 'react-dom/client'; + +const useReactRoot = (containerRef, shouldRender, renderJsx, heightRef) => { + const reactRootRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + if (!shouldRender) { + if (reactRootRef.current) { + const root = reactRootRef.current; + const savedHeight = heightRef.current; + reactRootRef.current = null; + + setTimeout(() => { + root.unmount(); + container.innerHTML = ''; + const placeholder = document.createElement('div'); + placeholder.className = 'example-driver-placeholder'; + placeholder.style.height = savedHeight + 'px'; + container.appendChild(placeholder); + }, 0); + } + return; + } + + const wasPlaceholder = container.querySelector('.example-driver-placeholder'); + if (wasPlaceholder) { + wasPlaceholder.remove(); + } + + const root = document.createElement('div'); + root.className = 'example-driver-runner'; + container.appendChild(root); + const reactRoot = createRoot(root); + reactRootRef.current = reactRoot; + reactRoot.render(renderJsx); + + requestAnimationFrame(() => { + const h = container.getBoundingClientRect().height; + if (h > 0) { + heightRef.current = h; + } + }); + }, [shouldRender, renderJsx, containerRef, heightRef]); +}; + +export default useReactRoot; diff --git a/src/index.js b/src/index.js index 144956a..93cc7be 100644 --- a/src/index.js +++ b/src/index.js @@ -1,235 +1,6 @@ -import React, {useEffect, useRef, useState, useMemo, memo} from 'react'; -import {createRoot} from 'react-dom/client'; -import classnames from 'classnames'; -import theme from './theme'; -import Highlight, {Prism} from "prism-react-renderer"; -import ErrorBoundary from '@kne/react-error-boundary'; -import uniqueId from 'lodash/uniqueId'; -import {transform as _transform} from '@babel/standalone'; -import CodeEditor from '@monaco-editor/react'; +import ExampleDriver from './components/ExampleDriver'; import monacoLoader from '@monaco-editor/loader'; -import {useDebouncedCallback} from 'use-debounce'; -import './style.scss' - -const HighlightCode = ({code}) => { - return - {({tokens, getLineProps, getTokenProps}) => (<> - {tokens.map((line, i) => (
- {line.map((token, key) => ())} -
))} - )} -
-}; - -const ErrorComponent = memo(({error}) => { - return (
- {error &&
{typeof error === 'string' ? error : error?.message}
} -
); -}); - -const LiveCode = ({code, scope, title, description, contextComponent}) => { - const [_code, setCode] = useState(code), [error, setError] = useState(null), [codeOpen, setCodeOpen] = useState(false), [minHeight, setMinHeight] = useState(0), - containerRef = useRef(null); - const reactRootRef = useRef(null); - const [renderJsx, setRenderJsx] = useState(null); - const [compiledCode, setCompiledCode] = useState(null); - const [shouldRender, setShouldRender] = useState(false); - const currentScope = useMemo(() => scope.filter(({component, name}) => !!component && !!name), [scope]); - - // 防抖编译函数,避免频繁编译 - const compileCode = useDebouncedCallback((codeToCompile) => { - if (!codeToCompile) { - setCompiledCode(null); - return; - } - - try { - setError(null); - const transformCode = _transform(codeToCompile, {presets: ['es2015', 'react']}).code; - setCompiledCode(transformCode); - } catch (e) { - setError(e); - setCompiledCode(null); - } - }, 500); - - useEffect(() => { - compileCode(_code); - }, [_code, compileCode]); - - // 视口检测 - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - // 部分进入视口时开始渲染 - if (entry.isIntersecting) { - setShouldRender(true); - } - // 完全离开视口时先清空 renderJsx 让 React unmount - if (!entry.isIntersecting && entry.intersectionRatio === 0) { - setRenderJsx(null); - setShouldRender(false); - } - }); - }, {threshold: [0, 1]}); - - observer.observe(container); - - return () => { - observer.disconnect(); - }; - }, []); - - useEffect(() => { - if (!compiledCode || !shouldRender) return; - - try { - setError(null); - // eslint-disable-next-line no-new-func - const runnerFunction = new Function('React', 'render', ...currentScope.map(({name}) => name), compiledCode) - const Component = contextComponent || (({children}) => { - return children; - }); - runnerFunction(React, jsx => setRenderJsx({jsx}), ...currentScope.map(({component}) => component)); - } catch (e) { - setError(e); - } - }, [compiledCode, currentScope, contextComponent, shouldRender]); - - // 渲染逻辑 - 统一处理渲染和卸载 - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - // 需要卸载时(shouldRender 为 false 或 renderJsx 为 null) - if (!shouldRender || !renderJsx) { - if (reactRootRef.current) { - const root = reactRootRef.current; - reactRootRef.current = null; - // 使用微任务延迟卸载,等待 React 完成当前渲染周期 - queueMicrotask(() => { - root.unmount(); - if (container) { - container.innerHTML = ''; - } - }); - } - return; - } - - // 渲染新内容 - if (reactRootRef.current) { - reactRootRef.current.unmount(); - } - container.innerHTML = ''; - - const root = document.createElement('div'); - root.className = 'example-driver-runner'; - container.appendChild(root); - const reactRoot = createRoot(root); - reactRootRef.current = reactRoot; - reactRoot.render(renderJsx); - setMinHeight((height) => { - return Math.max(height, root.getBoundingClientRect().height); - }); - }, [shouldRender, renderJsx]); - - return <> -
- {/* React 组件通过 createRoot 动态渲染到这里 */} -
-
- {title} -
- setCodeOpen(!codeOpen)}> - {codeOpen ? '' : '< >'} - -
- {codeOpen ? (<> -
-
- importStatement ? importStatement : (name ? `import * as ${name} from '${packageName}';` : `import '${packageName}';`)) - .join('\n')} -`}/> -
- -
- {error && } - ) : null} - ; -}; - -const MiniCode = ({code, qrcodeUrl, scope, title, description}) => { - const [codeOpen, setCodeOpen] = useState(false); - return <> -
-
-
示例
-
请扫描二维码查看示例程序
-
-
-
- {title} -
- setCodeOpen(!codeOpen)}> - {codeOpen ? '' : '< >'} - -
- {codeOpen ? (<> -
-
- importStatement ? importStatement : (name ? `import * as ${name} from '${packageName}';` : `import '${packageName}';`)) - .join('\n')} -`}/> -
- -
- ) : null} - -}; - -const DriverItem = ({isFull, contextComponent, list}) => { - return
- {list.map((props) => { - return
- {props.hasOwnProperty('qrcodeUrl') ? : - } -
- })} -
-}; - -const ExampleDriver = ({list, isFull, contextComponent, className, ...props}) => { - const groupList = isFull === true ? [list] : [list.filter((item, index) => index % 2 === 0), list.filter((item, index) => index % 2 !== 0)]; - return (
- {groupList.map((item, index) => )} -
); -}; +import './style.scss'; export default ExampleDriver; export const config = monacoLoader.config; diff --git a/src/locale/en-US.js b/src/locale/en-US.js new file mode 100644 index 0000000..10cccc6 --- /dev/null +++ b/src/locale/en-US.js @@ -0,0 +1,7 @@ +const locale = { + 'CodePanel.loading': 'Loading code editor...', + 'MiniCode.example': 'Example', + 'MiniCode.scanQrcode': 'Please scan the QR code to view the example' +}; + +export default locale; diff --git a/src/locale/zh-CN.js b/src/locale/zh-CN.js new file mode 100644 index 0000000..d9e0e0e --- /dev/null +++ b/src/locale/zh-CN.js @@ -0,0 +1,7 @@ +const locale = { + 'CodePanel.loading': '正在加载代码编辑器...', + 'MiniCode.example': '示例', + 'MiniCode.scanQrcode': '请扫描二维码查看示例程序' +}; + +export default locale; diff --git a/src/withLocale.js b/src/withLocale.js new file mode 100644 index 0000000..bfb5cb6 --- /dev/null +++ b/src/withLocale.js @@ -0,0 +1,14 @@ +import {createWithIntlProvider} from '@kne/react-intl'; +import zhCN from './locale/zh-CN'; +import enUS from './locale/en-US'; + +const withLocale = createWithIntlProvider({ + defaultLocale: 'zh-CN', + defaultMessage: zhCN, + messages: { + 'zh-CN': zhCN, + 'en-US': enUS + } +}); + +export default withLocale;