diff --git a/README.md b/README.md new file mode 100644 index 0000000..1aa5be8 --- /dev/null +++ b/README.md @@ -0,0 +1,430 @@ + +# example-driver + + +### 描述 + +用于在线展示和编辑React组件 + + +### 安装 + +```shell +npm i --save @kne/example-driver +``` + + +### 概述 + +@kne/example-driver 是一个用于在线展示和编辑 React 组件的工具库,特别适合用于组件库文档、教程演示和技术文档中。它提供了实时代码预览和编辑功能,让用户可以直接在浏览器中查看和修改组件代码,无需搭建完整的开发环境。 + +核心特性包括实时代码编辑、即时代码预览、语法高亮显示、错误边界处理和灵活的布局控制。支持两种展示模式:LiveCode 模式提供完整的在线编辑和实时预览能力,MiniCode 模式则通过二维码引导用户在移动端查看示例。内置 Monaco Editor 提供专业的代码编辑体验,使用 Prism 实现代码语法高亮,并通过 Debounce 优化性能,避免频繁重新渲染。 + +适用于组件库文档网站、在线教程和培训、技术博客和文档、以及 React 组件展示平台。通过动态加载和实时编译,让文档中的示例代码真正"活"起来,用户可以直接修改代码并立即看到效果,大大提升了学习效率和用户体验。 + +技术亮点方面,项目采用 Babel Standalone 实现浏览器端的代码编译,支持 ES2015 和 React 预设,无需后端转换。错误边界机制确保代码错误不会影响整个页面,提供友好的错误提示。支持自定义上下文组件,方便在不同场景中嵌入示例代码。Monaco Editor 配置暴露给外部,允许深度定制编辑器行为。响应式布局设计,支持单列和双列模式,适应不同屏幕尺寸。 + + +### 示例(全屏) + +#### 示例代码 + +- 基础使用 +- 展示 ExampleDriver 组件的基本用法,包含简单的计数器示例 +- _ExampleDriver(@kne/current-lib_example-driver)[import * as _ExampleDriver from "@kne/example-driver"],(@kne/current-lib_example-driver/dist/index.css),antd(antd),remoteLoader(@kne/remote-loader) + +```jsx +const {default: ExampleDriver} = _ExampleDriver; + +// 示例代码字符串 +const code = ` +const { Button, Card, Space } = antd; +const { useState } = React; + +const Component = () => { + const [count, setCount] = useState(0); + return ( +
+ +

计数器示例

+ + + {count} + + + +
+
+ ); +}; + +render(); +`; + +render(); + +``` + +- LiveCode 模式 +- 实时代码编辑和预览模式,可以直接在浏览器中修改代码并查看效果 +- _ExampleDriver(@kne/current-lib_example-driver)[import * as _ExampleDriver from "@kne/example-driver"],(@kne/current-lib_example-driver/dist/index.css),antd(antd),remoteLoader(@kne/remote-loader) + +```jsx +const {default: ExampleDriver} = _ExampleDriver; + +const code = ` +const { Button, Input, List, Space, Tag } = antd; +const { useState } = React; + +const Component = () => { + const [text, setText] = useState(''); + const [items, setItems] = useState(['学习 React', '编写组件']); + + const addItem = () => { + if (text.trim()) { + setItems([...items, text.trim()]); + setText(''); + } + }; + + const removeItem = (index) => { + setItems(items.filter((_, i) => i !== index)); + }; + + return ( +
+ + + setText(e.target.value)} + placeholder="输入事项..." + onPressEnter={addItem} + /> + + + ( + removeItem(index)}> + 删除 + + ]} + > + {item} + + )} + /> + +
+ ); +}; + +render(); +`; + +render(); + +``` + +- MiniCode 模式 +- 二维码预览模式,适用于移动端场景,通过二维码引导用户查看示例 +- _ExampleDriver(@kne/current-lib_example-driver)[import * as _ExampleDriver from "@kne/example-driver"],(@kne/current-lib_example-driver/dist/index.css),antd(antd),remoteLoader(@kne/remote-loader) + +```jsx +const {default: ExampleDriver} = _ExampleDriver; + +const code = ` +const { Form, Input, Button, Space } = antd; +const { useState } = React; + +const Component = () => { + const [form] = Form.useForm(); + const [data, setData] = useState([]); + + const onFinish = (values) => { + setData([...data, values]); + form.resetFields(); + }; + + return ( +
+
+ + + + + + + + + + + + +
+ + {data.length > 0 && ( +
+

已提交数据:

+ {data.map((item, index) => ( +

{item.name} - {item.email}

+ ))} +
+ )} +
+ ); +}; + +render(); +`; + +render(); + +``` + +- 自定义上下文 +- 使用 contextComponent 属性自定义示例的上下文组件 +- _ExampleDriver(@kne/current-lib_example-driver)[import * as _ExampleDriver from "@kne/example-driver"],(@kne/current-lib_example-driver/dist/index.css),antd(antd),remoteLoader(@kne/remote-loader) + +```jsx +const {default: ExampleDriver} = _ExampleDriver; + +const code = ` +const { Button, Card, Space, Switch, Tag } = antd; +const { useState } = React; + +// 自定义上下文组件 +const CustomContext = ({ children }) => { + const [theme, setTheme] = useState('light'); + const colors = { + light: { bg: '#fff', color: '#000', border: '#d9d9d9' }, + dark: { bg: '#1a1a1a', color: '#fff', border: '#434343' } + }; + const style = { + padding: '12px', + background: colors[theme].bg, + color: colors[theme].color, + borderRadius: '4px' + }; + + return ( +
+ + setTheme(checked ? 'dark' : 'light')} + checkedChildren="🌙" + unCheckedChildren="☀️" + /> + + {theme === 'dark' ? '暗色主题' : '亮色主题'} + + + {children} +
+ ); +}; + +const Component = () => { + return ( + +

这是上下文包裹的内容,会根据主题自动改变颜色。

+
+ ); +}; + +render(); +`; + +render( (
+ {children} +
), + scope: [{ + name: 'antd', packageName: 'antd', component: antd + }] +}]}/>); + +``` + +- 双列布局 +- 使用 isFull 属性控制布局,默认双列展示多个示例 +- _ExampleDriver(@kne/current-lib_example-driver)[import * as _ExampleDriver from "@kne/example-driver"],(@kne/current-lib_example-driver/dist/index.css),antd(antd),remoteLoader(@kne/remote-loader) + +```jsx +const {default: ExampleDriver} = _ExampleDriver; + +const example1 = ` +const { Button, Card } = antd; + +const Component = () => { + return ( + + + + + ); +}; + +render(); +`; + +const example2 = ` +const { Input, Card } = antd; + +const Component = () => { + return ( + + + + ); +}; + +render(); +`; + +const example3 = ` +const { Select, Card } = antd; +const { Option } = Select; + +const Component = () => { + return ( + + + + ); +}; + +render(); +`; + +const example4 = ` +const { Switch, Card } = antd; + +const Component = () => { + return ( + + + + ); +}; + +render(); +`; + +// 默认双列布局 +render(); + +``` + + +### API + +### ExampleDriver +用于展示和编辑 React 组件示例的主组件,支持 LiveCode 和 MiniCode 两种展示模式。 + +#### 属性说明 +| 属性名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| list | array | 是 | - | 示例列表数组,每个元素代表一个示例 | +| isFull | boolean | 否 | false | 是否全宽显示,true 时单列显示,false 时双列显示 | +| contextComponent | React Component | 否 | - | 自定义上下文组件,用于包裹渲染的代码内容 | +| className | string | 否 | - | 自定义 CSS 类名 | +| ...props | object | 否 | - | 其他 HTML div 元素的属性 | + +#### list 数组项属性说明 +| 属性名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| code | string | 是 | - | 示例代码字符串 | +| scope | array | 是 | - | 作用域数组,包含组件和包信息 | +| title | string | 是 | - | 示例标题 | +| description | string | 是 | - | 示例描述,支持 HTML 格式 | +| qrcodeUrl | string | 否 | - | 二维码图片 URL,传入此项则使用 MiniCode 模式 | +| contextComponent | React Component | 否 | - | 单个示例的自定义上下文组件 | + +#### scope 数组项属性说明 +| 属性名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| component | React Component | 是 | - | 要暴露给示例代码的组件 | +| name | string | 是 | - | 组件名称,在代码中使用 | +| packageName | string | 否 | - | 包名,用于生成导入语句 | +| importStatement | string | 否 | - | 自定义导入语句,覆盖自动生成的导入 | + +### LiveCode +提供实时代码编辑和预览功能的子组件(通过 ExampleDriver 自动使用)。 + +#### 属性说明 +| 属性名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| code | string | 是 | - | 要展示的代码 | +| scope | array | 是 | - | 作用域数组 | +| title | string | 是 | - | 标题 | +| description | string | 是 | - | 描述 | +| contextComponent | React Component | 否 | - | 上下文组件 | + +### MiniCode +显示二维码预览的只读模式子组件(通过 ExampleDriver 自动使用)。 + +#### 属性说明 +| 属性名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| code | string | 是 | - | 示例代码 | +| qrcodeUrl | string | 是 | - | 二维码图片地址 | +| scope | array | 是 | - | 作用域数组 | +| title | string | 是 | - | 标题 | +| description | string | 是 | - | 描述 | + +### config +Monaco Editor 的配置对象,用于自定义编辑器行为。 + +#### 方法说明 +| 方法名 | 参数 | 返回值 | 说明 | +|--------|------|--------|------| +| config | options: object | void | 配置 Monaco Editor 的加载选项,如模块路径等 | + diff --git a/doc/api.md b/doc/api.md new file mode 100644 index 0000000..b05f32b --- /dev/null +++ b/doc/api.md @@ -0,0 +1,61 @@ +### ExampleDriver +用于展示和编辑 React 组件示例的主组件,支持 LiveCode 和 MiniCode 两种展示模式。 + +#### 属性说明 +| 属性名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| list | array | 是 | - | 示例列表数组,每个元素代表一个示例 | +| isFull | boolean | 否 | false | 是否全宽显示,true 时单列显示,false 时双列显示 | +| contextComponent | React Component | 否 | - | 自定义上下文组件,用于包裹渲染的代码内容 | +| className | string | 否 | - | 自定义 CSS 类名 | +| ...props | object | 否 | - | 其他 HTML div 元素的属性 | + +#### list 数组项属性说明 +| 属性名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| code | string | 是 | - | 示例代码字符串 | +| scope | array | 是 | - | 作用域数组,包含组件和包信息 | +| title | string | 是 | - | 示例标题 | +| description | string | 是 | - | 示例描述,支持 HTML 格式 | +| qrcodeUrl | string | 否 | - | 二维码图片 URL,传入此项则使用 MiniCode 模式 | +| contextComponent | React Component | 否 | - | 单个示例的自定义上下文组件 | + +#### scope 数组项属性说明 +| 属性名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| component | React Component | 是 | - | 要暴露给示例代码的组件 | +| name | string | 是 | - | 组件名称,在代码中使用 | +| packageName | string | 否 | - | 包名,用于生成导入语句 | +| importStatement | string | 否 | - | 自定义导入语句,覆盖自动生成的导入 | + +### LiveCode +提供实时代码编辑和预览功能的子组件(通过 ExampleDriver 自动使用)。 + +#### 属性说明 +| 属性名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| code | string | 是 | - | 要展示的代码 | +| scope | array | 是 | - | 作用域数组 | +| title | string | 是 | - | 标题 | +| description | string | 是 | - | 描述 | +| contextComponent | React Component | 否 | - | 上下文组件 | + +### MiniCode +显示二维码预览的只读模式子组件(通过 ExampleDriver 自动使用)。 + +#### 属性说明 +| 属性名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| code | string | 是 | - | 示例代码 | +| qrcodeUrl | string | 是 | - | 二维码图片地址 | +| scope | array | 是 | - | 作用域数组 | +| title | string | 是 | - | 标题 | +| description | string | 是 | - | 描述 | + +### config +Monaco Editor 的配置对象,用于自定义编辑器行为。 + +#### 方法说明 +| 方法名 | 参数 | 返回值 | 说明 | +|--------|------|--------|------| +| config | options: object | void | 配置 Monaco Editor 的加载选项,如模块路径等 | diff --git a/doc/base.js b/doc/base.js new file mode 100644 index 0000000..8097db3 --- /dev/null +++ b/doc/base.js @@ -0,0 +1,32 @@ +const {default: ExampleDriver} = _ExampleDriver; + +// 示例代码字符串 +const code = ` +const { Button, Card, Space } = antd; +const { useState } = React; + +const Component = () => { + const [count, setCount] = useState(0); + return ( +
+ +

计数器示例

+ + + {count} + + + +
+
+ ); +}; + +render(); +`; + +render(); diff --git a/doc/context.js b/doc/context.js new file mode 100644 index 0000000..e322760 --- /dev/null +++ b/doc/context.js @@ -0,0 +1,60 @@ +const {default: ExampleDriver} = _ExampleDriver; + +const code = ` +const { Button, Card, Space, Switch, Tag } = antd; +const { useState } = React; + +// 自定义上下文组件 +const CustomContext = ({ children }) => { + const [theme, setTheme] = useState('light'); + const colors = { + light: { bg: '#fff', color: '#000', border: '#d9d9d9' }, + dark: { bg: '#1a1a1a', color: '#fff', border: '#434343' } + }; + const style = { + padding: '12px', + background: colors[theme].bg, + color: colors[theme].color, + borderRadius: '4px' + }; + + return ( +
+ + setTheme(checked ? 'dark' : 'light')} + checkedChildren="🌙" + unCheckedChildren="☀️" + /> + + {theme === 'dark' ? '暗色主题' : '亮色主题'} + + + {children} +
+ ); +}; + +const Component = () => { + return ( + +

这是上下文包裹的内容,会根据主题自动改变颜色。

+
+ ); +}; + +render(); +`; + +render( (
+ {children} +
), + scope: [{ + name: 'antd', packageName: 'antd', component: antd + }] +}]}/>); diff --git a/doc/example.json b/doc/example.json new file mode 100644 index 0000000..326c25f --- /dev/null +++ b/doc/example.json @@ -0,0 +1,120 @@ +{ + "isFull": true, + "list": [ + { + "title": "基础使用", + "description": "展示 ExampleDriver 组件的基本用法,包含简单的计数器示例", + "code": "./base.js", + "scope": [ + { + "name": "_ExampleDriver", + "packageName": "@kne/current-lib_example-driver", + "importStatement": "import * as _ExampleDriver from \"@kne/example-driver\"" + }, + { + "packageName": "@kne/current-lib_example-driver/dist/index.css" + }, + { + "name": "antd", + "packageName": "antd" + }, + { + "name": "remoteLoader", + "packageName": "@kne/remote-loader" + } + ] + }, + { + "title": "LiveCode 模式", + "description": "实时代码编辑和预览模式,可以直接在浏览器中修改代码并查看效果", + "code": "./livecode.js", + "scope": [ + { + "name": "_ExampleDriver", + "packageName": "@kne/current-lib_example-driver", + "importStatement": "import * as _ExampleDriver from \"@kne/example-driver\"" + }, + { + "packageName": "@kne/current-lib_example-driver/dist/index.css" + }, + { + "name": "antd", + "packageName": "antd" + }, + { + "name": "remoteLoader", + "packageName": "@kne/remote-loader" + } + ] + }, + { + "title": "MiniCode 模式", + "description": "二维码预览模式,适用于移动端场景,通过二维码引导用户查看示例", + "code": "./minicode.js", + "scope": [ + { + "name": "_ExampleDriver", + "packageName": "@kne/current-lib_example-driver", + "importStatement": "import * as _ExampleDriver from \"@kne/example-driver\"" + }, + { + "packageName": "@kne/current-lib_example-driver/dist/index.css" + }, + { + "name": "antd", + "packageName": "antd" + }, + { + "name": "remoteLoader", + "packageName": "@kne/remote-loader" + } + ] + }, + { + "title": "自定义上下文", + "description": "使用 contextComponent 属性自定义示例的上下文组件", + "code": "./context.js", + "scope": [ + { + "name": "_ExampleDriver", + "packageName": "@kne/current-lib_example-driver", + "importStatement": "import * as _ExampleDriver from \"@kne/example-driver\"" + }, + { + "packageName": "@kne/current-lib_example-driver/dist/index.css" + }, + { + "name": "antd", + "packageName": "antd" + }, + { + "name": "remoteLoader", + "packageName": "@kne/remote-loader" + } + ] + }, + { + "title": "双列布局", + "description": "使用 isFull 属性控制布局,默认双列展示多个示例", + "code": "./layout.js", + "scope": [ + { + "name": "_ExampleDriver", + "packageName": "@kne/current-lib_example-driver", + "importStatement": "import * as _ExampleDriver from \"@kne/example-driver\"" + }, + { + "packageName": "@kne/current-lib_example-driver/dist/index.css" + }, + { + "name": "antd", + "packageName": "antd" + }, + { + "name": "remoteLoader", + "packageName": "@kne/remote-loader" + } + ] + } + ] +} diff --git a/doc/layout.js b/doc/layout.js new file mode 100644 index 0000000..5fbc7fd --- /dev/null +++ b/doc/layout.js @@ -0,0 +1,85 @@ +const {default: ExampleDriver} = _ExampleDriver; + +const example1 = ` +const { Button, Card } = antd; + +const Component = () => { + return ( + + + + + ); +}; + +render(); +`; + +const example2 = ` +const { Input, Card } = antd; + +const Component = () => { + return ( + + + + ); +}; + +render(); +`; + +const example3 = ` +const { Select, Card } = antd; +const { Option } = Select; + +const Component = () => { + return ( + + + + ); +}; + +render(); +`; + +const example4 = ` +const { Switch, Card } = antd; + +const Component = () => { + return ( + + + + ); +}; + +render(); +`; + +// 默认双列布局 +render(); diff --git a/doc/livecode.js b/doc/livecode.js new file mode 100644 index 0000000..33eed0a --- /dev/null +++ b/doc/livecode.js @@ -0,0 +1,61 @@ +const {default: ExampleDriver} = _ExampleDriver; + +const code = ` +const { Button, Input, List, Space, Tag } = antd; +const { useState } = React; + +const Component = () => { + const [text, setText] = useState(''); + const [items, setItems] = useState(['学习 React', '编写组件']); + + const addItem = () => { + if (text.trim()) { + setItems([...items, text.trim()]); + setText(''); + } + }; + + const removeItem = (index) => { + setItems(items.filter((_, i) => i !== index)); + }; + + return ( +
+ + + setText(e.target.value)} + placeholder="输入事项..." + onPressEnter={addItem} + /> + + + ( + removeItem(index)}> + 删除 + + ]} + > + {item} + + )} + /> + +
+ ); +}; + +render(); +`; + +render(); diff --git a/doc/minicode.js b/doc/minicode.js new file mode 100644 index 0000000..e040b78 --- /dev/null +++ b/doc/minicode.js @@ -0,0 +1,56 @@ +const {default: ExampleDriver} = _ExampleDriver; + +const code = ` +const { Form, Input, Button, Space } = antd; +const { useState } = React; + +const Component = () => { + const [form] = Form.useForm(); + const [data, setData] = useState([]); + + const onFinish = (values) => { + setData([...data, values]); + form.resetFields(); + }; + + return ( +
+
+ + + + + + + + + + + + +
+ + {data.length > 0 && ( +
+

已提交数据:

+ {data.map((item, index) => ( +

{item.name} - {item.email}

+ ))} +
+ )} +
+ ); +}; + +render(); +`; + +render(); diff --git a/doc/summary.md b/doc/summary.md new file mode 100644 index 0000000..3e48adf --- /dev/null +++ b/doc/summary.md @@ -0,0 +1,7 @@ +@kne/example-driver 是一个用于在线展示和编辑 React 组件的工具库,特别适合用于组件库文档、教程演示和技术文档中。它提供了实时代码预览和编辑功能,让用户可以直接在浏览器中查看和修改组件代码,无需搭建完整的开发环境。 + +核心特性包括实时代码编辑、即时代码预览、语法高亮显示、错误边界处理和灵活的布局控制。支持两种展示模式:LiveCode 模式提供完整的在线编辑和实时预览能力,MiniCode 模式则通过二维码引导用户在移动端查看示例。内置 Monaco Editor 提供专业的代码编辑体验,使用 Prism 实现代码语法高亮,并通过 Debounce 优化性能,避免频繁重新渲染。 + +适用于组件库文档网站、在线教程和培训、技术博客和文档、以及 React 组件展示平台。通过动态加载和实时编译,让文档中的示例代码真正"活"起来,用户可以直接修改代码并立即看到效果,大大提升了学习效率和用户体验。 + +技术亮点方面,项目采用 Babel Standalone 实现浏览器端的代码编译,支持 ES2015 和 React 预设,无需后端转换。错误边界机制确保代码错误不会影响整个页面,提供友好的错误提示。支持自定义上下文组件,方便在不同场景中嵌入示例代码。Monaco Editor 配置暴露给外部,允许深度定制编辑器行为。响应式布局设计,支持单列和双列模式,适应不同屏幕尺寸。 diff --git a/example/craco.config.js b/example/craco.config.js new file mode 100644 index 0000000..ea84dba --- /dev/null +++ b/example/craco.config.js @@ -0,0 +1,32 @@ +const {CracoLibsExamplePlugin, env} = require('@kne/modules-dev'); +const aliasConfig = require('./webstorm.webpack.config'); +const packageJson = require('../package.json'); + +process.env.CI = false; + +module.exports = { + webpack: { + alias: aliasConfig.resolve.alias, configure: (webpackConfig) => { + const definePlugin = webpackConfig.plugins.find((plugin) => plugin.constructor.name === 'DefinePlugin'); + Object.assign(definePlugin.definitions['process.env'], { + DEFAULT_VERSION: `"${packageJson.version}"` + }); + return webpackConfig; + } + }, plugins: [{ + plugin: CracoLibsExamplePlugin, options: { + middleware: (moduleFederationConfig) => { + const shared = Object.assign({}, moduleFederationConfig.shared,{ + '@kne/current-lib_example-driver': { + singleton: true, requiredVersion: false + } + }); + return Object.assign({}, moduleFederationConfig, { + exposes: { + './components': env.manifestPath + }, shared + }) + } + } + }] +}; diff --git a/example/package.json b/example/package.json index 4241749..a406229 100644 --- a/example/package.json +++ b/example/package.json @@ -1,30 +1,55 @@ { - "name": "example-driver", - "homepage": "/example-driver", - "version": "0.0.0", - "private": true, - "scripts": { - "start": "node ../node_modules/react-scripts/bin/react-scripts.js start", - "build": "node ../node_modules/react-scripts/bin/react-scripts.js build", - "test": "node ../node_modules/react-scripts/bin/react-scripts.js test", - "eject": "node ../node_modules/react-scripts/bin/react-scripts.js eject" - }, + "name": "@kne-components/example-driver", + "version": "0.1.0", "dependencies": { - "@kne/example-driver": "file:..", + "@kne/current-lib_example-driver": "file:..", "react": "file:../node_modules/react", - "react-dom": "file:../node_modules/react-dom", - "react-scripts": "file:../node_modules/react-scripts" + "react-dom": "file:../node_modules/react-dom" + }, + "files": [ + "build" + ], + "scripts": { + "init": "npm install --legacy-peer-deps", + "start": "cross-env PORT=3020 MODULES_DEV_PUBLIC_URL=/ craco start", + "build": "cross-env COMPONENTS_NAME=example-driver MODULES_DEV_PUBLIC_URL=/example-driver craco build" }, "eslintConfig": { - "extends": "react-app" + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] }, - "browserslist": [ - ">0.2%", - "not dead", - "not ie <= 11", - "not op_mini all" - ], "devDependencies": { - "@kne/react-fetch": "^1.1.13" + "@craco/craco": "^7.1.0", + "@kne/axios-fetch": "^1.0.7", + "@kne/craco-module-federation": "^1.1.2", + "@kne/modules-dev": "^2.0.6", + "@kne/react-fetch": "^1.4.2", + "@kne/remote-loader": "^1.2.3", + "antd": "^5.13.3", + "axios": "^1.6.7", + "classnames": "^2.5.1", + "cross-env": "^7.0.3", + "http-proxy-middleware": "^2.0.6", + "husky": "^8.0.3", + "lint-staged": "^13.3.0", + "lodash": "^4.17.21", + "sass": "^1.75.0", + "prettier": "^2.8.8", + "react-router-dom": "^6.22.0", + "react-scripts": "^5.0.1" } } diff --git a/example/public/favicon.ico b/example/public/favicon.ico index a11777c..e69de29 100644 Binary files a/example/public/favicon.ico and b/example/public/favicon.ico differ diff --git a/example/public/favicon.svg b/example/public/favicon.svg new file mode 100644 index 0000000..8ee9c9e --- /dev/null +++ b/example/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/public/index.html b/example/public/index.html index ee3821b..ed1c3c4 100644 --- a/example/public/index.html +++ b/example/public/index.html @@ -1,48 +1,42 @@ - - - - - - - - - - + + + + + + + + - react-form-antd - - - - - -
- + manifest.json provides metadata used when your web app is installed on a + user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ + --> + + [@kne/example-driver] - KneUnion Libs + + + + +
+ + - To begin the development, run `npm start` or `yarn start`. - To create a production bundle, use `npm run build` or `yarn build`. - --> - diff --git a/example/public/logo192.png b/example/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/example/public/logo192.png differ diff --git a/example/public/logo512.png b/example/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/example/public/logo512.png differ diff --git a/example/public/manifest.json b/example/public/manifest.json index da47227..080d6c7 100644 --- a/example/public/manifest.json +++ b/example/public/manifest.json @@ -1,11 +1,21 @@ { - "short_name": "react-form", - "name": "react-form", + "short_name": "React App", + "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" } ], "start_url": ".", diff --git a/example/public/mock/data.json b/example/public/mock/data.json index d1e3193..e69de29 100644 --- a/example/public/mock/data.json +++ b/example/public/mock/data.json @@ -1,19 +0,0 @@ -{ - "code": 0, - "data": { - "list": [ - { - "id": 1, - "name": "1撒打算大的" - }, - { - "id": 2, - "name": "2大方大方大方" - }, - { - "id": 3, - "name": "3西西说大方水电费速度" - } - ] - } -} diff --git a/example/public/mock/error.json b/example/public/mock/error.json index 5dcbbbc..e69de29 100644 --- a/example/public/mock/error.json +++ b/example/public/mock/error.json @@ -1,20 +0,0 @@ -{ - "code": 502, - "msg": "错误信息", - "data": { - "list": [ - { - "id": 1, - "name": "1撒打算大的" - }, - { - "id": 2, - "name": "2大方大方大方" - }, - { - "id": 3, - "name": "3西西说大方水电费速度" - } - ] - } -} diff --git a/example/public/qrcode.jpg b/example/public/qrcode.jpg index 5e82546..e69de29 100644 Binary files a/example/public/qrcode.jpg and b/example/public/qrcode.jpg differ diff --git a/example/src/App.js b/example/src/App.js index 0e4b367..aa9b906 100644 --- a/example/src/App.js +++ b/example/src/App.js @@ -1,34 +1,29 @@ -import ExampleDriver from '@kne/example-driver'; -import * as ReactFetch from '@kne/react-fetch'; +import { HashRouter } from "react-router-dom"; +import createEntry from "@kne/modules-dev/dist/create-entry.modern"; +import "@kne/modules-dev/dist/create-entry.css"; +import readme from "readme"; -const App = () => { - return {\n console.log(config);\n return new Promise((resolve) => {\n setTimeout(() => {\n resolve({\n data: {\n code: 0,\n data: [\n {title: '数据一'},\n {title: '数据二'}\n ]\n }\n });\n }, 1000);\n });\n },\n loading: 'loading....',\n empty: '空',\n transformResponse: (response) => {\n const {data} = response;\n response.data = {\n code: data.code === 0 ? 200 : data.code, msg: data.msg, results: data.data\n };\n return response;\n }\n});\n\nconst Remote = createWithFetch({\n url: '/react-fetch/mock/data.json',\n params: {page: 1},\n updateType: 'nextPage'\n})(({data, send, reload, refresh, loadMore, requestParams}) => {\n return data.map((item, index) => {\n return
{item.title}
\n });\n});\n\nrender();\n\n" - }, { - "title": "基本用法", - "description": "注意:react-fetch 内部处理请求的时候只通过 code,msg,results来作为内部逻辑,code为200判定为请求成功,不为200时判定为错误,msg会传入到error组件,拿到results后,会将results作为业务组件的data属性\n如果后端的返回不满足上诉格式,需要在preset的transformResponse方法做转换适配\najax为一个axios实例,每个实例的拦截器可能不同,默认会在内部自动创建一个axios实例,但是没有任何拦截器,如果想给其添加拦截器,可以自行创建axios实例通过preset设置\npreset 可以单独放一个文件里,在入口文件顶部引入", - "isFull": false, - "scope": [{ - "name": "ReactFetch", "packageName": "@kne/react-fetch", "component": ReactFetch - }], - "code": "const {createWithFetch, preset} = ReactFetch;\n\npreset({\n ajax: (config) => {\n console.log(config);\n return new Promise((resolve) => {\n setTimeout(() => {\n resolve({\n data: {\n code: 0,\n data: [\n {title: '数据一'},\n {title: '数据二'}\n ]\n }\n });\n }, 1000);\n });\n },\n loading: 'loading....',\n empty: '空',\n transformResponse: (response) => {\n const {data} = response;\n response.data = {\n code: data.code === 0 ? 200 : data.code, msg: data.msg, results: data.data\n };\n return response;\n }\n});\n\nconst Remote = createWithFetch({\n url: '/react-fetch/mock/data.json',\n params: {page: 1},\n updateType: 'nextPage'\n})(({data, send, reload, refresh, loadMore, requestParams}) => {\n return data.map((item, index) => {\n return
{item.title}
\n });\n});\n\nrender();\n\n" - }, { - "title": "状态标签", - "description": "这里填写示例说明", - "qrcodeUrl": "/example-driver/qrcode.jpg", - "scope": [{ - "name": "miniCore", "packageName": "@kne/mini-core" - }, { - "name": "lodash", "packageName": "lodash" - }], - "code": "const {StateTag} = miniCore;\nconst BaseExample = () => {\n return 哈哈哈;\n};\n\nrender();\n\n" - }]}/> +const ExampleRoutes = createEntry.ExampleRoutes; + +const App = ({ globalPreset, ...props }) => { + return ( + + + + ); }; export default App; diff --git a/example/src/bootstrap.js b/example/src/bootstrap.js new file mode 100644 index 0000000..e5c19bb --- /dev/null +++ b/example/src/bootstrap.js @@ -0,0 +1,13 @@ +import { globalInit } from './preset'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); + +const renderRoot = async (App) => { + const globalPreset = await globalInit(); + return root.render(); +}; + +renderRoot(App); diff --git a/example/src/index.js b/example/src/index.js index 1d247ae..b93c7a0 100644 --- a/example/src/index.js +++ b/example/src/index.js @@ -1,7 +1 @@ -import '@kne/example-driver/dist/index.css'; -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; - -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render(); +import('./bootstrap'); diff --git a/example/src/preset.js b/example/src/preset.js new file mode 100644 index 0000000..6f0ca1a --- /dev/null +++ b/example/src/preset.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { preset as fetchPreset } from '@kne/react-fetch'; +import { Spin, Empty, message } from 'antd'; +import createAjax from '@kne/axios-fetch'; +import { preset as remoteLoaderPreset } from '@kne/remote-loader'; + +window.PUBLIC_URL = process.env.PUBLIC_URL; + +// url: 'https://registry.npmmirror.com', +// tpl: '{{url}}/@kne-components%2f{{remote}}/{{version}}/files/build', + +// url: 'https://cdn.jsdelivr.net', tpl: '{{url}}/npm/@kne-components/{{remote}}@{{version}}/build' + +const registry = { + url: 'https://uc.fatalent.cn', tpl: '{{url}}/packages/@kne-components/{{remote}}/{{version}}/build' +}; + +export const globalInit = async () => { + const ajax = createAjax({ + errorHandler: error => message.error(error) + }); + + const componentsCoreRemote = { + ...registry, remote: 'components-core', defaultVersion: '0.4.51' + }; + + remoteLoaderPreset({ + remotes: { + default: componentsCoreRemote, 'components-core': componentsCoreRemote, 'components-iconfont': { + ...registry, remote: 'components-iconfont', defaultVersion: '0.2.1' + }, 'example-driver': process.env.NODE_ENV === 'development' ? { + remote: 'example-driver', url: '/', tpl: '{{url}}' + } : { + ...registry, remote: 'example-driver', defaultVersion: process.env.DEFAULT_VERSION + } + } + }); + + + fetchPreset({ + ajax, loading: , error: null, empty: , transformResponse: (response) => { + const { data } = response; + response.data = { + code: data.code === 0 ? 200 : data.code, msg: data.msg, results: data.data + }; + return response; + } + }); + + return { + ajax, enums: {}, apis: {}, themeToken: { + colorPrimary: '#4183F0' + } + }; +}; diff --git a/example/src/setupTests.js b/example/src/setupTests.js new file mode 100644 index 0000000..b28b910 --- /dev/null +++ b/example/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; \ No newline at end of file diff --git a/example/webstorm.webpack.config.js b/example/webstorm.webpack.config.js new file mode 100644 index 0000000..49dbd2e --- /dev/null +++ b/example/webstorm.webpack.config.js @@ -0,0 +1,12 @@ +'use strict' +const path = require('path') + +module.exports = { + context: path.resolve(__dirname), + resolve: { + extensions: ['.js', '.jsx', '.json'], + alias: { + "@root": path.resolve("./src") + } + } +} diff --git a/package.json b/package.json index 4d9cc48..ffd528c 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,34 @@ { "name": "@kne/example-driver", - "version": "0.1.12", + "version": "0.1.13", "description": "用于在线展示和编辑React组件", + "syntax": { + "esmodules": true + }, + "types": "dist/index.d.ts", "main": "dist/index.js", "module": "dist/index.modern.js", "source": "src/index.js", "scripts": { - "init": "npm install && cd example && npm install", - "start": "run-p start:lib start:example", - "build": "run-s build:lib build:example", - "build:lib": "microbundle --no-compress --format modern,cjs --jsx React.createElement --jsxFragment React.Fragment", - "build:md": "create-md", + "init": "husky && npm run init-example", + "start": "run-p start:lib start:md start:example", + "build": "run-s build:lib build:md build:example", + "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", - "test": "run-s test:unit test:lint test:build", + "build:example": "cd example && npm run build", + "start:example": "cd example && npm run start", "test:build": "run-s build", "test:lint": "eslint .", "test:unit": "cross-env CI=1 react-scripts test --env=jsdom", "test:watch": "react-scripts test --env=jsdom", - "build:example": "cd example && npm run build", - "start:example": "cd example && npm run start", - "deploy": "gh-pages -d example/build", - "prettier": "prettier --config .prettierrc --write 'src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'" + "prettier": "prettier --config .prettierrc --write '{src/**/*,index,prompts}.{js,jsx,ts,tsx,json,css,scss}'", + "lint-staged": "npx lint-staged" }, "repository": { "type": "git", @@ -47,6 +55,7 @@ }, "homepage": "https://github.com/kne-union/react-fetch#readme", "peerDependencies": { + "@monaco-editor/loader": "*", "axios": ">=0.19.2", "prop-types": ">=15.x", "react": ">=16.x", @@ -54,18 +63,23 @@ }, "devDependencies": { "@kne/md-doc": "^0.1.8", - "axios": "^0.26.1", + "@kne/microbundle": "^0.15.5", + "@kne/modules-dev": "^2.1.18", + "antd": "^5.21.6", + "axios": "^1.12.0", "cross-env": "^7.0.3", - "gh-pages": "^3.2.3", - "husky": "^7.0.4", - "microbundle": "^0.14.2", - "node-sass": "^9.0.0", + "gh-pages": "^5.0.0", + "husky": "^9.0.11", "npm-run-all": "^4.1.5", "prettier": "^2.5.1", - "react-scripts": "^5.0.0" + "react-scripts": "^5.0.0", + "sass": "^1.97.3", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "dependencies": { "@babel/standalone": "^7.24.0", + "@kne/react-error-boundary": "^0.1.1", "@monaco-editor/react": "^4.6.0", "babel-standalone": "^6.26.0", "classnames": "^2.3.1", diff --git "a/prompts/\345\221\275\345\220\215\347\244\272\344\276\213\347\274\226\345\206\231\347\244\272\344\276\213\346\217\217\350\277\260.md" "b/prompts/\345\221\275\345\220\215\347\244\272\344\276\213\347\274\226\345\206\231\347\244\272\344\276\213\346\217\217\350\277\260.md" new file mode 100644 index 0000000..c7952d3 --- /dev/null +++ "b/prompts/\345\221\275\345\220\215\347\244\272\344\276\213\347\274\226\345\206\231\347\244\272\344\276\213\346\217\217\350\277\260.md" @@ -0,0 +1,50 @@ +# 任务:完善示例文档的标题和描述 + +## 目标 +根据 `doc/example.json` 中 `code` 字段引用的代码实现内容,完善该文件中的 `title` 和 `description` 字段。 + +## 具体要求 + +### 1. 分析代码实现 +- 仔细阅读 `doc/example.json` 中每个示例项的 `code` 字段引用的代码文件 +- 理解每个代码示例的核心功能和实现方式 +- 识别组件的主要用途和特点 + +### 2. 编写标题 (title) +- **简洁明了**:用4-8个字概括示例的核心功能 +- **突出重点**:体现示例的最主要特性 +- **用户视角**:从使用者的角度命名 +- **避免技术术语**:使用通俗易懂的表达 + +### 3. 编写描述 (description) +- **功能说明**:清晰描述示例展示的功能 +- **使用场景**:说明适用的业务场景 +- **特点优势**:突出实现的特点或优势 +- **长度控制**:建议在20-50字之间 + +## 示例格式 + +### 原始结构 +```json +{ + "code": "./form-modal.js", + "title": "", + "description": "" +} +``` + +### 优化后示例 +```json +{ + "code": "./form-modal.js", + "title": "模态框表单", + "description": "在模态框中展示表单,支持弹窗内数据提交和验证" +} +``` + +## 注意事项 +- 不要修改 `code` 字段的路径引用 +- 确保每个示例都有完整的 `title` 和 `description` +- 描述要准确反映代码实现的功能 +- 保持所有示例的命名风格一致 +- 重点突出用户体验和实用价值 \ No newline at end of file diff --git "a/prompts/\345\233\275\351\231\205\345\214\226.md" "b/prompts/\345\233\275\351\231\205\345\214\226.md" new file mode 100644 index 0000000..9ec750e --- /dev/null +++ "b/prompts/\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/\346\267\273\345\212\240ts\347\261\273\345\236\213\345\243\260\346\230\216.md" "b/prompts/\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/\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/\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/\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/\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/\347\224\237\346\210\220\346\226\207\346\241\243.md" "b/prompts/\347\224\237\346\210\220\346\226\207\346\241\243.md" new file mode 100644 index 0000000..b02891b --- /dev/null +++ "b/prompts/\347\224\237\346\210\220\346\226\207\346\241\243.md" @@ -0,0 +1,79 @@ +# 任务:生成项目文档 + +## 目标 +根据代码实现生成完整的项目文档,包括项目概述文件 (`doc/summary.md`) 和 API 文档 (`doc/api.md`)。 + +## 文档生成要求 + +### doc/summary.md - 项目概述 +#### 格式要求 +- **禁止使用 h1、h2 标题**:直接从内容开始编写 +- **无需标题**:不需要"项目概述"等标题文字 +- **不包含依赖项说明**:专注于项目特点和功能介绍 +- **吸引用户**:突出项目优势和使用价值 + +#### 内容结构 +1. **项目简介**:简明扼要介绍项目的主要功能和定位 +2. **核心特性**:列出3-5个最突出的特点或优势 +3. **适用场景**:描述适用的业务场景和使用场景 +4. **技术亮点**:重点说明技术创新或设计优势 + +#### 写作风格 +- **简洁明了**:用通俗语言表达,避免过度技术化 +- **用户导向**:从使用者角度描述价值 +- **重点突出**:强调解决什么问题,带来什么便利 + +### doc/api.md - API 文档 +#### 格式要求 +- **无需标题**:不添加"API文档"等标题 +- **使用 h3 及以下级别**:组件标题用 h3,子部分用 h4、h5 +- **优先使用表格格式**:API 参数和属性用表格展示 +- **不包含示例代码**:专注于 API 说明,不提供代码示例 + +#### 内容结构 +```markdown +### 组件名称 +组件功能描述文字 + +#### 属性说明 +| 属性名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| prop1 | string | 是 | - | 属性说明 | +| prop2 | number | 否 | 0 | 属性说明 | + +#### 方法说明 +| 方法名 | 参数 | 返回值 | 说明 | +|--------|------|--------|------| +| method1 | data: object | void | 方法说明 | +``` + +#### API 文档要点 +- **完整性**:涵盖所有公开的 API +- **准确性**:类型、必填项、默认值要准确 +- **清晰性**:说明文字简洁明确 +- **一致性**:命名和格式保持一致 + +## 生成流程 + +### 1. 分析代码结构 +- 阅读 `src/index.js` 了解导出的组件 +- 分析组件的 TypeScript 类型定义 (`src/index.d.ts`) +- 理解组件的属性和功能 + +### 2. 提取 API 信息 +- 从组件代码中提取 props 定义 +- 识别方法的参数和返回值 +- 确定属性的类型和默认值 + +### 3. 编写文档内容 +- 按照格式要求编写两个文档 +- 确保内容准确性和完整性 +- 检查格式规范符合要求 + +## 严格注意事项 +- **禁止使用 h1、h2 标签**:严格遵守 markdown 标题级别限制 +- **summary.md 无标题**:直接从内容开始 +- **api.md 无文档标题**:直接从第一个组件 h3 标题开始 +- **使用表格格式**:API 说明必须使用表格 +- **不包含示例代码**:专注于文档说明 +- **保持格式一致性**:确保所有文档格式符合要求 \ No newline at end of file diff --git "a/prompts/\347\224\237\346\210\220\350\257\255\350\250\200\345\214\205.md" "b/prompts/\347\224\237\346\210\220\350\257\255\350\250\200\345\214\205.md" new file mode 100644 index 0000000..e583148 --- /dev/null +++ "b/prompts/\347\224\237\346\210\220\350\257\255\350\250\200\345\214\205.md" @@ -0,0 +1,125 @@ +# 任务:生成和更新国际化语言包 + +## 目标 +从代码中抽取中文文案,生成完整的国际化语言包,并同步更新到英文语言包。 + +## 任务要求 + +### 1. 抽取中文文案 +#### 扫描范围 +- **组件文件**:扫描 `src/` 目录下的所有 `.js` 组件文件 +- **模板字符串**:提取模板字符串中的中文文本 +- **字符串字面量**:提取字符串常量中的中文内容 +- **JSX 文本**:提取 JSX 中的中文显示文本 + +#### 抽取规则 +- **纯中文文本**:只提取包含中文字符的字符串 +- **过滤无关内容**:忽略 CSS 类名、技术术语、调试信息 +- **保持原意**:确保提取的文案保持原始含义 +- **去重处理**:相同文案只保留一个 key + +### 2. 生成语言包结构 +#### zh-CN.js 格式 +```javascript +const locale = { + // 已有的 key 保持不变 + submit: '提交', + cancel: '取消', + + // 新增的 key + addText: '添加', + deleteText: '删除', + // ... 其他新增文案 +}; + +export default locale; +``` + +#### Key 命名规范 +- **驼峰命名**:使用 camelCase 命名规则 +- **语义化**:key 名称要能表达文案含义 +- **简洁明了**:避免过长的 key 名称 +- **一致性**:相似功能使用相同命名模式 + +### 3. 翻译到英文版本 +#### 翻译原则 +- **准确翻译**:确保英文翻译准确传达中文含义 +- **自然表达**:使用符合英文习惯的表达方式 +- **保持一致**:相同概念使用一致的英文词汇 +- **简洁明了**:避免冗长复杂的表达 + +#### en-US.js 格式 +```javascript +const locale = { + // 与 zh-CN.js 对应的英文翻译 + submit: 'Submit', + cancel: 'Cancel', + complete: 'Complete', + next: 'Next', + addText: 'Add', + deleteText: 'Delete', + // ... 其他对应的英文文案 +}; + +export default locale; +``` + +## 实施步骤 + +### 第一步:代码扫描 +1. 遍历 `src/` 目录下的所有组件文件 +2. 使用正则表达式匹配中文文本 +3. 收集所有中文文案并去重 +4. 过滤掉不需要国际化的内容 + +### 第二步:更新中文语言包 +1. 打开 `src/locale/zh-CN.js` +2. 保留现有的 key-value 对 +3. 添加新发现的文案和对应的 key +4. 确保格式正确,导出语句完整 + +### 第三步:更新英文语言包 +1. 打开 `src/locale/en-US.js` +2. 为所有中文 key 添加对应的英文翻译 +3. 确保翻译准确、自然、一致 +4. 保持与中文版本相同的 key 结构 + +### 第四步:质量检查 +1. **完整性检查**:确保中文和英文版本 key 一致 +2. **准确性检查**:验证翻译的准确性 +3. **格式检查**:确保文件格式符合要求 +4. **语法检查**:确保 JavaScript 语法正确 + +## 严格注意事项 +- **不修改原始文件**:绝对不要修改任何组件源代码 +- **保留现有内容**:不要删除或修改已有的语言包内容 +- **保持文件结构**:维持两个语言包文件的完整结构 +- **确保导出正确**:保持 `export default locale` 语句 +- **格式一致性**:确保代码格式和缩进一致 + +## 常见中文文案类型 +- **按钮文本**:确认、取消、添加、删除、保存等 +- **提示信息**:成功、失败、警告、信息提示等 +- **表单标签**:用户名、密码、邮箱、手机号等 +- **操作描述**:编辑、查看、删除、复制等 +- **状态说明**:加载中、已完成、进行中等 +- **错误信息**:必填项验证、格式错误、网络错误等 + +## 输出格式示例 +```javascript +// zh-CN.js +const locale = { + // 原有内容 + submit: '提交', + cancel: '取消', + + // 新增内容 + confirmDelete: '确认删除', + deleteSuccess: '删除成功', + addNewItem: '添加新项目', + loading: '加载中...', + required: '此字段为必填项' +}; + +export default locale; +``` \ No newline at end of file diff --git "a/prompts/\347\273\204\344\273\266\347\244\272\344\276\213\347\274\226\345\206\231.md" "b/prompts/\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..74c5b64 --- /dev/null +++ "b/prompts/\347\273\204\344\273\266\347\244\272\344\276\213\347\274\226\345\206\231.md" @@ -0,0 +1,299 @@ +# 组件示例编写提示词 + +本提示词用于编写遵循特定目录结构规范的 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. 示例代码规范 + +### 导入方式 + +- 使用 `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/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..102138b --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,224 @@ +import { ReactNode, ComponentType, CSSProperties, HTMLAttributes } from 'react'; + +/** + * Scope项定义,用于向示例代码中注入组件和模块 + */ +export interface ScopeItem { + /** + * 组件对象或值 + */ + component?: any; + + /** + * 在示例代码中使用的变量名 + */ + name?: string; + + /** + * 包名或模块名,用于生成导入语句 + */ + packageName?: string; + + /** + * 自定义导入语句,如果提供则不自动生成 + */ + importStatement?: string; +} + +/** + * LiveCode组件的属性 + */ +export interface LiveCodeProps { + /** + * 示例代码字符串 + */ + code: string; + + /** + * 作用域数组,包含注入到示例代码中的组件和模块 + */ + scope: ScopeItem[]; + + /** + * 示例标题 + */ + title: string; + + /** + * 示例描述,支持HTML格式 + */ + description: string; + + /** + * 自定义上下文组件,用于包裹渲染的示例内容 + */ + contextComponent?: ComponentType<{ children?: ReactNode }>; +} + +/** + * MiniCode组件的属性 + */ +export interface MiniCodeProps { + /** + * 示例代码字符串 + */ + code: string; + + /** + * 二维码图片URL,用于移动端预览 + */ + qrcodeUrl: string; + + /** + * 作用域数组,包含注入到示例代码中的组件和模块 + */ + scope: ScopeItem[]; + + /** + * 示例标题 + */ + title: string; + + /** + * 示例描述,支持HTML格式 + */ + description: string; +} + +/** + * 示例项属性,可以是LiveCode或MiniCode + */ +export interface ExampleItem { + /** + * 示例标题 + */ + title: string; + + /** + * 示例描述,支持HTML格式 + */ + description: string; + + /** + * 示例代码 + */ + code: string; + + /** + * 作用域数组 + */ + scope: ScopeItem[]; + + /** + * 二维码图片URL,如果提供则使用MiniCode模式 + */ + qrcodeUrl?: string; + + /** + * 自定义上下文组件 + */ + contextComponent?: ComponentType<{ children?: ReactNode }>; +} + +/** + * DriverItem组件的属性(内部使用) + */ +export interface DriverItemProps { + /** + * 是否全宽显示 + */ + isFull?: boolean; + + /** + * 自定义上下文组件 + */ + contextComponent?: ComponentType<{ children?: ReactNode }>; + + /** + * 示例列表 + */ + list: ExampleItem[]; +} + +/** + * Monaco编辑器配置选项 + */ +export interface MonacoConfigOptions { + /** + * 模块路径配置 + */ + paths?: Record; + + /** + * 其他配置选项 + */ + [key: string]: any; +} + +/** + * Monaco编辑器配置函数 + */ +export interface MonacoConfig { + /** + * 配置Monaco编辑器 + * @param options 配置选项 + */ + (options: MonacoConfigOptions): void; +} + +/** + * ExampleDriver组件的属性 + */ +export interface ExampleDriverProps extends HTMLAttributes { + /** + * 示例列表数组 + */ + list: ExampleItem[]; + + /** + * 是否全宽显示,true时单列显示,false时双列显示 + * 默认:false + */ + isFull?: boolean; + + /** + * 自定义上下文组件,用于包裹所有示例的渲染内容 + */ + contextComponent?: ComponentType<{ children?: ReactNode }>; + + /** + * 自定义CSS类名 + */ + className?: string; + + /** + * 其他HTML div元素属性 + */ + [key: string]: any; +} + +/** + * LiveCode组件声明 + */ +export declare const LiveCode: ComponentType; + +/** + * MiniCode组件声明 + */ +export declare const MiniCode: ComponentType; + +/** + * DriverItem组件声明 + */ +export declare const DriverItem: ComponentType; + +/** + * ExampleDriver主组件 + * 用于展示和编辑React组件示例 + */ +export declare const ExampleDriver: ComponentType; + +/** + * Monaco编辑器配置对象 + */ +export declare const config: MonacoConfig; diff --git a/src/index.js b/src/index.js index cb52e75..c4bfbe7 100644 --- a/src/index.js +++ b/src/index.js @@ -1,26 +1,15 @@ -import React, {useEffect, useRef, useState} from 'react'; -import ReactDOM from "react-dom/client"; +import React, {useEffect, useRef, useState, useMemo, memo} from 'react'; import classnames from 'classnames'; import theme from './theme'; import Highlight, {Prism} from "prism-react-renderer"; -import get from "lodash/get"; +import ErrorBoundary from '@kne/react-error-boundary'; import uniqueId from 'lodash/uniqueId'; import {transform as _transform} from '@babel/standalone'; -import {useDebouncedCallback} from 'use-debounce'; import CodeEditor from '@monaco-editor/react'; import monacoLoader from '@monaco-editor/loader'; +import {useDebouncedCallback} from 'use-debounce'; import './style.scss' -const renderCallback = (el, callback) => { - const ProxyComponent = () => { - useEffect(() => { - callback(); - }, []); - return el; - }; - return ; -}; - const HighlightCode = ({code}) => { return { }; +const ErrorComponent = memo(({error}) => { + return (
+ {error &&
{error}
} +
); +}); + const LiveCode = ({code, scope, title, description, contextComponent}) => { const [_code, setCode] = useState(code), [error, setError] = useState(null), [codeOpen, setCodeOpen] = useState(false), [minHeight, setMinHeight] = useState(0), runnerRef = useRef(null); - const currentScope = scope.filter(({component, name}) => !!component && !!name); - const debounced = useDebouncedCallback((_code) => { - const runner = runnerRef.current, root = ReactDOM.createRoot(runner), - beforeHeight = get(runner, 'clientHeight', 0); - const promise = Promise.resolve() - .then(() => { - return _transform(_code, {presets: ['es2015', 'react']}).code; - }) - .then(runCode => { - return new Function('React', 'render', ...currentScope.map(({name}) => name), runCode); - }) - .then(runnerFunction => { - runnerFunction(React, (customComponent) => { - const Component = contextComponent || (({children}) => { - return children; - }); - root.render({renderCallback(customComponent, () => { - //只允许预览区域的高度增加,防止在编辑代码的时候预览区域高度反复跳动 - setMinHeight(Math.max(get(runner, 'clientHeight', 0), beforeHeight)); - })}); - }, ...currentScope.map(({component}) => component)); - setError(null); - }) - .catch(error => { - setError(error); - }); - return () => { - promise.then(() => { - root.unmount(); - }); - }; - }, 1000); + const [renderJsx, setRenderJsx] = useState(null); + const [compiledCode, setCompiledCode] = useState(null); + 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(() => { - setCode(code); - setCodeOpen(false); - }, [code]); + if (!compiledCode) 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]); useEffect(() => { - return debounced(_code); - }, [_code]); + runnerRef.current && setMinHeight(runnerRef.current.clientHeight); + }, [renderJsx]); + return <>
+ > + {renderJsx} +
{title} @@ -110,9 +112,7 @@ ${scope.map(({
-
- {error &&
{error.message}
} -
+ {error && } ) : null} ; }; diff --git a/src/package-manifest.json b/src/package-manifest.json new file mode 100644 index 0000000..201ffc1 --- /dev/null +++ b/src/package-manifest.json @@ -0,0 +1,39 @@ +{ + "description": "用于在线展示和编辑React组件的工具库", + "modules": { + "default": { + "description": "主组件,用于展示和编辑React组件示例,支持LiveCode和MiniCode两种展示模式", + "type": "ReactNode", + "props": { + "list": { + "description": "示例列表数组,每个元素代表一个示例", + "type": "array" + }, + "isFull": { + "description": "是否全宽显示,true时单列显示,false时双列显示,默认双列", + "type": "boolean" + }, + "contextComponent": { + "description": "自定义上下文组件,用于包裹所有示例的渲染内容", + "type": "ReactNode" + }, + "className": { + "description": "自定义CSS类名", + "type": "string" + } + } + }, + "config": { + "description": "Monaco Editor配置对象,用于自定义Monaco编辑器的加载选项和行为", + "type": "function", + "parameters": [ + { + "name": "options", + "description": "配置选项对象", + "type": "object" + } + ], + "returns": "void" + } + } +}