不少朋友在接触到 GenUI SDK 生成式 UI 能力之后都眼前一亮,迫不及待地去尝鲜。但试用完很快就遇到了一个现实问题:生成式 UI 生成出来的页面看着效果不错,可一放到自己的业务系统里,样式、色调、交互全都对不上,和现有产品 UI 格格不入。
于是大家纷纷来问:能不能让 GenUI SDK 的样式风格,适配我们项目自己的组件库风格?
答案是:可以。自 1.3.0 起,GenUI SDK 完成了物料解耦------框架不再内置组件,而是通过独立的「物料包」接入任意组件库。官方已发布基于 OpenTiny Vue、Element Plus、OpenTiny NG 的物料包。
- 官网地址:opentiny.design/genui-sdk
- GitHub 源码:github.com/opentiny/ge...
今天,我们就来亲手打造一个属于自己的物料库,主角是 Vue 技术栈的 Naive UI 组件库。先来预览一下最终成果:

为什么需要自定义 GenUI 物料库?
前端组件库生态十分丰富,不同组件库各有优势,适配不同的项目场景与开发需求。很多存量项目都根据自身需求选用了不同的组件库------比如今天要用的 Naive UI,但官方物料暂时并不支持它。
更进一步说,不同的系统对同一个组件库的使用方式也各有侧重:看板系统集中使用图表组件,信息收集场景频繁使用表单组件。这时候,精简物料库------只按需引入你真正需要的组件------会获得更好的生成体验。
拿今天要制作的 Naive UI 物料库举例:它的用途是生成一个登录页面,因此只需要简单的表单组件和按钮就够用了。仅按需引入需要的组件,可以压缩提示词的大小,对生成速度和生成效果都有帮助。
这时候,自定义物料库就派上用场了。
基于 Naive UI 搭建 GenUI SDK 物料库
开发一个物料库其实十分简单,只需要准备两份"材料":
- 组件清单(materials) :渲染器要把
componentName翻译成真实组件 - 组件说明书(meta):大模型要知道有哪些组件、每个组件有哪些属性
我们要做一个极简的 Naive UI 物料库,只包含:
- 表单组件:NInput 输入框、NSelect 选择器、NButton 按钮
- 表单容器:NForm 表单、NFormItem 表单项
- 卡片容器:NCard(也是默认的包裹组件)
- 图标:自封装的 NIconSvg(Step 4 讲解),搭配 2 个图标------放大镜 SearchOutline、对勾 CheckmarkOutline
麻雀虽小,五脏俱全。物料库的全部秘密,也就是就藏在这几个文件里。
完整 demo 工程(含全部组件与测试代码)已放到 GitHub:opentiny/genui-sdk-demos。接下来就是手把手的指导过程。
项目结构
先来剧透一下整体结构,后面每个 Step 会逐个填进去:
bash
genui-materials-naive-ui/
├── src/
│ ├── index.ts # 包入口,统一导出 meta 与 materials
│ ├── materials/
│ │ ├── index.ts # materials 子路径入口
│ │ ├── materials.ts # 组装 IMaterials(组件表 + 默认值映射)
│ │ └── components/
│ │ ├── index.ts
│ │ ├── components.ts # componentName -> 组件 注册表
│ │ └── NIconSvg.vue # 图标封装组件(Step 4)
│ └── meta/
│ ├── index.ts # meta 子路径入口
│ ├── meta.ts # 组装 IMaterialsMeta(协议 + 白名单)
│ ├── white-list.ts # 允许 LLM 使用的 componentName 白名单
│ └── bundle.json # 组件协议描述(LLM 说明书)
├── test/ # 本地测试工程
│ ├── main.ts
│ ├── App.vue # GenuiConfigProvider + GenuiRenderer 联调
│ └── fetch-schema-stream.ts # 流式请求 LLM 并解析 Schema
├── index.html # 测试 dev server 入口
├── vite.config.ts # 库模式构建配置
├── vite.test.config.ts # 本地测试 dev server 配置
├── .env # LLM 接口地址与 Key(本地测试用)
└── package.json
Step 1:初始化项目
bash
mkdir genui-materials-naive-ui && cd genui-materials-naive-ui
npm init -y
npm install @opentiny/genui-sdk-core
npm install vue naive-ui @vicons/ionicons5
npm install -D typescript vite vite-plugin-dts @vitejs/plugin-vue @opentiny/genui-sdk-vue
@opentiny/genui-sdk-vue 是渲染器,本地测试时要用到(放在 devDependencies,构建产物不依赖它)。
package.json 关键字段(注意用 exports 声明 materials 和 meta 两个子路径):
json
{
"name": "genui-materials-naive-ui",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
"./materials":{ "types": "./dist/materials.d.ts", "import": "./dist/materials.js" },
"./meta": { "types": "./dist/meta.d.ts", "import": "./dist/meta.js" }
},
"scripts": {
"build": "vite build",
"dev": "vite --config vite.test.config.ts"
},
"dependencies": {
"@opentiny/genui-sdk-core": "^1.3.0",
"@vicons/ionicons5": "^0.13.0",
"naive-ui": "^2.45.0",
"vue": "^3.5.32"
},
"devDependencies": {
"@opentiny/genui-sdk-vue": "^1.3.0",
"@vitejs/plugin-vue": "^6.0.6",
"typescript": "~5.9.3",
"vite": "^8.0.8",
"vite-plugin-dts": "^5.0.3"
}
}
Step 2:组件注册表(materials)
渲染器拿到 Schema 里的 componentName,就要靠这张表找到真实组件:
ts
// src/materials/components/components.ts
import type { Component } from 'vue';
import { NButton, NCard, NForm, NFormItem, NInput, NSelect } from 'naive-ui';
export interface IComponents {
[key: string]: Component;
}
export const components: IComponents = {
NButton,
NCard,
NForm,
NFormItem,
NInput,
NSelect,
};
图标组件
NIconSvg我们放到 Step 4 再注册,这里先注册基础表单组件。
再把注册表组装成渲染器需要的 IMaterials:
requiredCompleteFieldSelectors可以配置缓冲字段,让特殊字段在完整后再显示。buildMaterialDefaultValueMap会根据bundle.json里的属性生成默认值映射,流式渲染时字段不全也能自动补全。
ts
// src/materials/materials.ts
import { buildMaterialDefaultValueMap, type IMaterials } from '@opentiny/genui-sdk-core';
import { materialsMeta } from '../meta';
import { components } from './components';
const requiredCompleteFieldSelectors = [];
export { components };
export const materials: IMaterials = {
components,
requiredCompleteFieldSelectors,
defaultPropsMap: buildMaterialDefaultValueMap(materialsMeta),
};
就这么简单。一行组件,就是一个物料。
Step 3:组件说明书(meta)
这是最关键的一步------告诉大模型「组件应该怎么用」。我们以 NInput 为例,写它的协议描述(放在 src/meta/bundle.json):
json
{
"data": {
"framework": "Vue",
"materials": {
"components": [
{
"name": { "zh_CN": "输入框" },
"component": "NInput",
"description": "通过鼠标或键盘输入字符",
"npm": {
"package": "naive-ui",
"exportName": "NInput",
"destructuring": true
},
"schema": {
"properties": [
{
"name": "0",
"label": { "zh_CN": "基础属性" },
"content": [
{
"property": "modelValue",
"label": { "text": { "zh_CN": "绑定值" } },
"description": { "zh_CN": "绑定值" },
"required": true,
"type": "string",
"cols": 12
},
{
"property": "placeholder",
"label": { "text": { "zh_CN": "占位文本" } },
"description": { "zh_CN": "输入框占位文本" },
"required": false,
"type": "string",
"cols": 12
}
]
}
],
"events": {
"onUpdate:modelValue": {
"label": { "zh_CN": "绑定值改变时触发" },
"description": { "zh_CN": "绑定值改变时触发" }
}
}
}
}
]
}
}
}
每个字段都对应大模型最终要生成的 Schema 结构,所以要写清楚、写准确。各字段含义:
component:组件名,对应渲染时的componentName,必须与组件注册表的 key 一致。name:组件显示名,用于配置面板与分组展示。description:组件说明,LLM 生成时的主要依据,写得越细越准。npm:组件来源包信息(包名、导出名),用于代码生成与按需引入。schema.properties:属性分组描述,决定配置面板展示哪些属性。schema.events:组件事件,供 LLM 生成事件绑定。
然后把 bundle.json 和白名单组装成 materialsMeta:
ts
// src/meta/white-list.ts
export const whiteList = [
'NInput', 'NSelect', 'NButton', 'NForm', 'NFormItem', 'NCard',
'div', 'span', 'Text',
];
ts
// src/meta/meta.ts
import type { IMaterialsMeta, IMaterialsProtocol } from '@opentiny/genui-sdk-core';
import bundleJson from './bundle.json' with { type: 'json' };
import { whiteList } from './white-list';
export const materialsMeta: IMaterialsMeta = {
materials: [bundleJson] as unknown as IMaterialsProtocol[],
wrapperComponent: 'NCard',
whiteList,
examples: [],
rules: [],
};
三个入口文件收尾(推荐的目录结构):
ts
// src/materials/components/index.ts
export * from './components';
// src/materials/index.ts
export * from './materials';
// src/meta/index.ts
export * from './meta';
// src/index.ts
export * from './meta';
export * from './materials';
Step 4:图标物料怎么加?
加图标物料其实和加普通组件完全一样,只多一步:封装一个图标组件,把 name 属性映射到具体图标。官方 vue-element-plus 物料包的 ElIconSvg 就是个现成范本。
1. 封装图标组件
新建 src/materials/components/NIconSvg.vue,按 name 从 @vicons/ionicons5 里取对应图标:
vue
<!-- src/materials/components/NIconSvg.vue -->
<script setup lang="ts">
import { computed, type Component } from 'vue';
import * as Icons from '@vicons/ionicons5';
const props = withDefaults(
defineProps<{
name: string;
}>(),
{ name: '' },
);
const iconComponent = computed(() => {
return (Icons as Record<string, Component | unknown>)[props.name] || null;
});
</script>
<template>
<component :is="iconComponent" v-if="iconComponent" />
</template>
2. 注册进组件注册表
ts
// src/materials/components/components.ts
import NIconSvg from './NIconSvg.vue';
export const components: IComponents = {
NButton,
NCard,
NForm,
NFormItem,
NIconSvg,
NInput,
NSelect,
};
3. 在 bundle.json 里描述它
图标组件就是个普通组件,属性只有一个 name,用 SelectIconConfigurator 就能在配置面板里直接选图标:
json
{
"name": { "zh_CN": "图标" },
"component": "NIconSvg",
"description": "图标组件,name 为图标名,例如 SearchOutline、CheckmarkOutline",
"schema": {
"properties": [
{
"name": "0",
"label": { "zh_CN": "基础属性" },
"content": [
{
"property": "name",
"label": { "text": { "zh_CN": "图标名称" } },
"description": { "zh_CN": "图标名称,例如 SearchOutline(搜索)、CheckmarkOutline(对勾)" },
"required": true,
"type": "string",
"cols": 12,
"widget": { "component": "SelectIconConfigurator", "props": {} }
}
]
}
]
}
}
4. 加入白名单
ts
// src/meta/white-list.ts
export const whiteList = [
'NInput', 'NSelect', 'NButton', 'NForm', 'NFormItem', 'NCard', 'NIconSvg',
'div', 'span', 'Text',
];
这样大模型就能随手生成带图标的按钮了,比如 NButton 的 icon 插槽里塞一个 NIconSvg。
Step 5:构建配置
用 Vite 库模式,把 materials、meta 打成独立入口,并让 vue、naive-ui 保持 external:
ts
// vite.config.ts
import path from 'node:path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import vue from '@vitejs/plugin-vue';
import packageJson from './package.json';
export default defineConfig({
plugins: [vue(), dts()],
build: {
lib: {
entry: {
index: path.resolve(__dirname, './src/index.ts'),
materials: path.resolve(__dirname, './src/materials/index.ts'),
meta: path.resolve(__dirname, './src/meta/index.ts'),
},
formats: ['es'],
fileName: (_, entryName) => `${entryName}.js`,
},
sourcemap: true,
rollupOptions: {
external: [
...Object.keys(packageJson.dependencies || {}),
],
},
},
});
Step 6:本地验证
写完了物料库,先别急着发版------在本地把它跑起来,看看 LLM 能不能真的生成 Naive UI 的界面。物料包里带一个 test/ 测试工程,用 Vite 单独起一个 dev server。
vite.test.config.ts 起一个独立的测试 dev server,端口 5175:
ts
// vite.test.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// 用于本地验证 test/ 目录下的 App.vue
export default defineConfig({
plugins: [vue()],
server: {
port: 5175,
open: true,
},
});
入口 index.html(在包根目录,脚本指向 test/main.ts):
html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>genui-materials-naive-ui · Test</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/test/main.ts"></script>
</body>
</html>
test/main.ts:
ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
test/App.vue------核心联调页面:输入问题 -> 调 LLM 生成 Schema -> GenuiRenderer 渲染成真实的 Naive UI 界面:
vue
<script setup lang="ts">
import { ref } from 'vue';
import { GenuiRenderer, GenuiConfigProvider } from '@opentiny/genui-sdk-vue';
import { genPrompt } from '@opentiny/genui-sdk-core';
import { components } from '../src/materials';
import { materialsMeta } from '../src/meta';
import { fetchSchemaStream } from './fetch-schema-stream';
// naive-ui 物料注册表:组件映射(渲染器通过它解析 componentName -> 组件)
const materials = { components };
const inputText = ref('');
const schema = ref<any>({ componentName: 'Page', children: [] });
const rendererKey = ref(0);
const generating = ref(false);
// 通过 core 包生成任务说明(system prompt),注入物料协议与白名单规则
const systemPrompt = genPrompt('Vue', materialsMeta);
console.log('genPrompt 生成结果(前 200 字符):', systemPrompt.slice(0, 200));
const handleSend = async () => {
if (!inputText.value.trim() || generating.value) return;
generating.value = true;
schema.value = '';
rendererKey.value++;
const userInput = inputText.value;
inputText.value = '';
try {
await fetchSchemaStream(
import.meta.env.VITE_DEEPSEEK_API_URL,
import.meta.env.VITE_DEEPSEEK_API_KEY,
userInput,
systemPrompt,
(schemaChunk) => {
schema.value += schemaChunk;
},
);
} catch (error) {
console.error('请求失败:', error);
} finally {
generating.value = false;
}
};
</script>
<template>
<GenuiConfigProvider :materials="materials">
<div class="demo-container">
<div class="input-group">
<input
v-model="inputText"
placeholder="请输入问题,例如:帮我生成一个登录表单"
@keyup.enter="handleSend"
/>
<button :disabled="generating" @click="handleSend">{{ generating ? '生成中...' : '发送' }}</button>
</div>
<GenuiRenderer :content="schema" :key="rendererKey" />
</div>
</GenuiConfigProvider>
</template>
<style scoped>
.demo-container {
padding: 16px;
box-sizing: border-box;
}
.input-group {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 8px 16px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
test/fetch-schema-stream.ts------用 PatternExtractor 从 LLM 的流式输出里解析出 schemaJson 片段:
ts
import { PatternExtractor } from '@opentiny/genui-sdk-core';
/**
* 将用户输入与 genPrompt 生成的任务说明(systemPrompt)发送到 LLM,
* 并流式解析其中的 schemaJson 片段。
*/
export async function fetchSchemaStream(
url: string,
apiKey: string,
userInput: string,
systemPrompt: string,
onSchemaUpdate: (schemaChunk: string) => void,
): Promise<void> {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userInput },
],
model: 'deepseek-v4-flash',
thinking: {
type: 'disabled',
},
stream: true,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body!.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
const patternExtractor = new PatternExtractor({
onNormalWrite: () => {},
onHandledWrite: (value) => onSchemaUpdate(value),
});
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
while (true) {
const lineEndIndex = buffer.indexOf('\n');
if (lineEndIndex === -1) break;
const line = buffer.slice(0, lineEndIndex).trim();
buffer = buffer.slice(lineEndIndex + 1);
if (!line.startsWith('data:')) continue;
const dataStr = line.slice(5).trim();
if (dataStr === '[DONE]') {
return;
}
try {
const chunk = JSON.parse(dataStr);
const content = chunk.choices?.[0]?.delta?.content;
if (!content) continue;
patternExtractor.handleContent(content);
} catch (e) {
console.error('解析后端数据失败:', e, dataStr);
}
}
}
} finally {
reader.releaseLock();
}
}
最后配一下 .env(LLM 的接口地址和 Key):
ini
VITE_DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
VITE_DEEPSEEK_API_KEY=sk-your-deepseek-api-key
启动:
bash
npm run dev
浏览器会自动打开 http://localhost:5175,输入「帮我生成一个登录表单」,就能看到 LLM 生成的 Naive UI 界面了------也就是文章开头提到的预览效果。
Step 7:发布
就这样,一个简单的物料库就开发完毕了。你甚至可以修改包名,然后把它发布到 npm 仓上:
bash
npm run build
npm publish --access public
就这样。你的第一个物料库,上线了。
物料库项目接入与配置
物料库开发完成后,接入应用只需两步。
安装:
bash
npm install genui-materials-naive-ui naive-ui
前端渲染 ------用 GenuiConfigProvider 注入 materials:
vue
<script setup lang="ts">
import { GenuiChat, GenuiConfigProvider } from '@opentiny/genui-sdk-vue';
import { materials } from 'genui-materials-naive-ui/materials';
</script>
<template>
<GenuiConfigProvider :materials="materials">
<GenuiChat />
</GenuiConfigProvider>
</template>
服务端生成 ------用 genPrompt 把 materialsMeta 拼进系统提示词:
ts
import { genPrompt } from '@opentiny/genui-sdk-core';
import { materialsMeta } from 'genui-materials-naive-ui/meta';
const systemPrompt = genPrompt('Vue', materialsMeta);
至此,你就能在对话里让 AI 生成 Naive UI 风格的界面了。也就是文章开篇的效果~
提升物料库可用性的三个实用技巧
- 把描述写详细 :组件和属性的
description越具体,LLM 生成越准确,这是性价比最高的优化。 - 提供示例 Schema :在
materialsMeta.examples里放一个典型表单,LLM 会照葫芦画瓢。 - 声明缓冲字段 :在
materials.requiredCompleteFieldSelectors里声明[componentName=NSelect] > props > options这类字段路径,流式渲染更稳。
AI 赋能物料库高效迭代
除了按照上述步骤自行开发物料库外,其实还有一条"捷径"------使用 AI 助力。
具体做法:
- 将官方的 TinyVue 物料库发送给 Agent(物料库地址:github.com/opentiny/ge...
- 将你想要自定义的物料库的组件文档链接也发送给 Agent(例如:www.naiveui.com/en-US/light...
- 把你想要集成的组件告诉 Agent,或者把你的场景描述一下,让 Agent 自行梳理
- 让 Agent 参考官方的 TinyVue 物料库编写物料信息
这样你就可以很快地完成一个自定义物料库了(demo 库中的物料信息就是使用 AI 帮忙生成的)。
总结
物料解耦,意味着 GenUI SDK 的组件生态从此完全开放。无论是 Element Plus、Ant Design,还是你公司内部的私有组件库,都可以用这套三件套快速接入:
组件映射(materials)+ 组件说明书(meta)+ 注入使用(ConfigProvider / genPrompt)
完整的 demo 工程(genui-materials-naive-ui,含全部组件与测试代码)已在 GitHub 开源,可以直接克隆或参考:
- Demo 仓库 PR:github.com/opentiny/ge...
- 官方文档:自定义物料库
如果你也做出了好用的物料库,欢迎在 GitHub 分享你的作品,或者提交 PR 把它收录进官方物料包!
关于OpenTiny NEXT
OpenTiny NEXT 是一套企业智能前端开发解决方案,以生成式 UI 和 WebMCP 两大核心技术为基础,对现有传统的 TinyVue 组件库、TinyEngine 低代码引擎等产品进行智能化升级,构建出面向 Agent 应用的前端 NEXT-SDKs、AI Extension、TinyRobot智能组件库、GenUI等新产品,实现AI理解用户意图自主完成任务,加速企业应用的智能化改造。
欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:opentiny.design
GenUI SDK 代码仓库:github.com/opentiny/ge... (欢迎star ⭐)
如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~如果你有任何问题,欢迎在评论区留言交流!