Compare commits

..

7 Commits

Author SHA1 Message Date
OpenTiny f629a5e6d4 原子化重构框架搭建
Match-id-b776ab82a6cb2da3c6af12ee6db97ea612b0dd93
2024-05-21 16:25:18 +08:00
OpenTiny 0ccd5c050c feat: 添加区块构建代码
Match-id-3848a3dcf0de114905b43261099894c9f3a83f51
2023-12-14 17:18:52 +08:00
OpenTiny 9b7bf4eb09 feat: 同步外部代码到out分支
Match-id-d527f2f763b5c3cadc34d8fe46b7781afb38e3a9
2023-12-14 17:14:20 +08:00
OpenTiny 88509ee8db refactor: 支持 pnpm
Match-id-d6d5411f705f8d59f001f96ad3d043a8164839c0
2023-09-28 18:18:57 +08:00
OpenTiny 552782671e update readme (#6)
* fix: add logo and fix readme

* fix: update readme

Match-id-2d1dc77d489c5d0651d4b26ddae718019716cd48
2023-09-28 17:39:50 +08:00
OpenTiny cc35c5f673 fix: add logo and fix readme (#5)
Match-id-03efb7c8a1b05fa9b99ac88a6a82ea2ae5236bfb
2023-09-28 17:39:32 +08:00
OpenTiny 418b96687a fix: fix build error (#1)
* fix: fix build error

* fix: fix server dependence

Match-id-22cc76beb68e512f012e9fa4219458bd548b4c81
2023-09-28 17:39:10 +08:00
2574 changed files with 50199 additions and 203918 deletions

View File

@ -1,168 +0,0 @@
---
name: tinyengine-dsl-generator
description: Use when creating or modifying TinyEngine low-code applications - generating page, block, or app DSL (JSON schemas), converting designs/screenshots to DSL, or debugging generated TinyEngine JSON.
---
# TinyEngine DSL Generator
Generate conformant DSL (JSON) for the TinyEngine low-code platform: **pages**, **blocks**, and **apps**. This file is a router — load the reference files on demand for detail instead of reading everything up front.
## Quick Reference
| Task | Do |
| ------------------ | ----------------------------------------------------- |
| Generate page DSL | Describe components, layout, interactions → §Workflow |
| Generate block DSL | Describe reusable functionality + configurable props |
| Generate app DSL | Describe multi-page structure + shared componentsMap |
| From screenshot | Describe layout → map to components (§Design-to-DSL) |
| Lookup a component | `node scripts/query_components.mjs props <Name>` |
| Validate output | `bash scripts/validate_all.sh <file>` (required) |
## Workflow
1. **Understand the goal** — Page (components / state / methods / lifeCycles), Block (reusable, exposes a props `schema`), or App (pages + `componentsMap` + `meta`).
2. **Gather requirements** — name / route / title; component hierarchy; state; event handlers; data sources. Blocks additionally: exposed props, emitted events. Apps additionally: all pages, shared `componentsMap`.
3. **Load only the reference you need:**
| Need | File |
| ----------------------------------------------------------- | ---------------------------------------------------------- |
| Schema structure, TS interfaces, reserved names, prop types | [protocol.md](references/protocol.md) |
| Component props/events, or a component not listed here | [components.md](references/components.md) · `query_components.mjs` |
| List page / form page / layout / interaction templates | [patterns.md](references/patterns.md) |
⚠️ **Before generating any page with interactions**, read the event-binding section of [protocol.md](references/protocol.md). Event handlers are the #1 error source; the compact Critical Rules table below is a reminder, not a substitute for the full ❌/✅ example.
4. **Generate** — follow the Page skeleton + property types below. Full Page/Block/Component interfaces are in protocol.md.
5. **Validate** (required) — see §Validate.
6. **Run the checklist** before handing off — see §Pre-Generation Checklist.
### Page skeleton (anchor)
```json
{
"componentName": "Page",
"fileName": "PageName",
"meta": { "id": 1, "title": "...", "router": "...", "creator": "...", "isHome": false, "parentId": "0", "rootElement": "div", "group": "staticPages" },
"state": {},
"methods": {},
"lifeCycles": {},
"children": []
}
```
### Property value types
- **Literal**: `"text"`, `123`, `true`
- **JSExpression**: `{"type":"JSExpression","value":"this.state.count"}` — bindings, conditions, **event handlers**
- **JSFunction**: `{"type":"JSFunction","value":"function(){}"}`**only** inside `methods` / `lifeCycles`
- **i18n**: `{"type":"i18n","key":"app.title"}`
- **JSResource**: `{"type":"JSResource","value":"this.utils.format()"}`
### Referencing a block
```json
{ "componentName": "BlockFileName", "componentType": "block", "id": "block-001", "props": { "title": "value" } }
```
Inside the block: read `this.props.xxx`, emit via `this.emit('eventName', data)`.
## Critical Rules (common pitfalls)
These cause silent failures. Full ❌/✅ JSON examples live in [protocol.md](references/protocol.md); the checklist below enforces them.
| Rule | ❌ Wrong | ✅ Right |
| ------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| **Event bindings** | `"onClick":{"type":"JSFunction",...}`; or `JSExpression.value` = `"function…"` | `"onClick":{"type":"JSExpression","value":"this.handleX"}` — put the body in `methods` as `JSFunction` |
| **Method params** | `function(filter){...}` | `function(event, filter){...}`; binding `"params":["'all'"]` → call `handleX(event,'all')` |
| **Lifecycle name** | `"mounted":{...}` | `"onMounted":{"type":"JSFunction","value":"function onMounted(){...}"}` |
| **Two-way binding**| `modelValue` with **no** `model` | `"model":true` (v-model) or `"model":{"prop":"x"}` (v-model:x) |
| **Page editable** | `"occupier": {...}` | `"occupier": null` |
| **CSS class** | `props.class` | `props.className` |
### Event bindings — the full pattern (highest-frequency error)
The function body lives in `methods` (`JSFunction`); the event only **references** it (`JSExpression`). `event` is always the first arg; `params` append after.
```json
"methods": {
"handleDelete": {
"type": "JSFunction",
"value": "function(event, id) { this.state.list = this.state.list.filter(x => x.id !== id); }"
}
},
"children": [{
"componentName": "TinyButton",
"props": {
"text": "删除",
"onClick": { "type": "JSExpression", "value": "this.handleDelete", "params": ["123"] }
}
}]
```
The binding above calls `handleDelete(event, 123)`. ❌ Never put a `JSFunction` on an event, and never put a `function(){}` body inside a `JSExpression.value` — both silently break the handler.
**Memory aid:** `JSExpression` = reference (`this.fn`) · `JSFunction` = definition (`function(){}`). Events use references; methods use definitions.
## Validate (required)
```bash
bash .agents/skills/tinyengine-dsl-generator/scripts/validate_all.sh <output-file>
```
`validate_all.sh` chains three checks — do **not** rely on `validate_dsl.mjs` alone (it misses event-binding errors). Fix and re-run until all three pass; never hand off unvalidated output.
| Stage | Script | Catches |
| -------------- | ------------------------ | ------------------------------------------------------------------------------- |
| Structure | validate_dsl.mjs | Required fields, Page/Block `componentName`, meta, `class` vs `className`, app/page id types |
| Event bindings | check_event_bindings.mjs | `JSFunction` on an event, or a function body in `JSExpression.value` |
| CSS | check_css.mjs | Malformed `css` strings |
## Pre-Generation Checklist
- [ ] Event bindings use `JSExpression`; no `value` starts with `"function"`; function bodies live in `methods` / `lifeCycles`
- [ ] Event methods take `event` as the first parameter; `params` append after it
- [ ] Lifecycle names start with `on` (`onMounted`, …); `setup` is the only exception
- [ ] `modelValue` declares `model` (`true` for standard v-model)
- [ ] `occupier` is `null`
- [ ] All `id`s are unique; CSS classes use `className`, not `class`
- [ ] **App schema** `id` and `meta.appId` are integers (`918`, not `"918"`) — apps.js persists `meta.appId` as string internally, keep the DSL integer
- [ ] **Page** `app` reference is a string (`"918"`, not `918`) — pages.js queries with `appId.toString()`; a numeric `app` won't be found by `list()`. Page's own `id` is a NanoID string assigned by the server
## Component lookup
Don't load `bundle.json` (≈1 MB) by hand. Query it:
```bash
node scripts/query_components.mjs list # all components
node scripts/query_components.mjs props TinyButton # one component's props (fuzzy match)
node scripts/query_components.mjs cat 表单 # components in a category
node scripts/query_components.mjs search 表格 # full-text search
```
## File output
- **Apps**`mockServer/data/apps/<app-name>.json`
- **Pages**`mockServer/data/pages/<PageName>.json`
- **Blocks**`mockServer/data/blocks/<BlockName>.json`
Pages and blocks are saved with an **outer wrapper** around the DSL: page files wrap the Page DSL in `page_content` (plus `name`, `id`, `app`, `route`, `tenant`, `parentId`, `group`, `isPage`, `isHome`); block files wrap the Block DSL in `content` (plus `id`, `label`, `framework`, `path`, `public`, `is_published`). The validators auto-unwrap both, so you can validate either the wrapper or the inner DSL directly.
## Design-to-DSL
From a description or screenshot: identify layout regions → map visuals to components → extract interactions → define state + handlers → apply `className` / `style`.
## Troubleshooting
| Problem | Check |
| ----------------- | ---------------------------------------------------------------------- |
| Input not working | `modelValue` declares `model` (`true` for standard v-model) |
| Event not firing | `JSExpression` (not `JSFunction`); method exists in `methods` |
| Page not editable | `occupier` is `null` |
| Wrong params | first param is always `event`; `params` append after |
## Resources
- [protocol.md](references/protocol.md) — schema spec, TS interfaces, reserved names, property types, slots, full ❌/✅ examples
- [components.md](references/components.md) — component catalog (props/events); supplement with `query_components.mjs`
- [patterns.md](references/patterns.md) — list/form page, layout, interaction templates
- `scripts/``validate_all.sh` (run this), `validate_dsl.mjs`, `check_event_bindings.mjs`, `check_css.mjs`, `validate_page.mjs`, `query_components.mjs`

View File

@ -1,474 +0,0 @@
# TinyEngine Components Reference
本文档包含 TinyEngine 可用组件的快速参考。
## 组件来源
组件清单位于项目根目录的 `designer-demo/public/mock/bundle.json` 文件中。
## 组件分类
| 分类 | 组件数量 | 说明 |
| ------------ | -------- | ----------------- |
| general | 1+ | 通用基础组件 |
| html | 10 | HTML 原生元素 |
| 容器组件 | 2 | 布局容器 |
| 图表组件 | 12 | 数据可视化 |
| 组件 | 8 | 业务组件 |
| 评分组件 | 1 | 评分输入 |
| 进度条 | 1 | 进度显示 |
| 骨架屏 | 1 | 加载占位 |
| 滑块组件 | 1 | 滑块输入 |
| 步骤条 | 1 | 步骤导航 |
| element-plus | 7 | Element Plus 组件 |
| Other | 33+ | 其他组件 |
## 常用基础组件
### 按钮 (TinyButton)
```typescript
interface TinyButtonProps {
text: string // 按钮文字
type: 'primary' | 'success' | 'warning' | 'danger' | 'info'
size: 'medium' | 'small' | 'mini'
disabled: boolean
plain: boolean // 朴素按钮
round: boolean // 圆角
circle: boolean // 圆形
loading: boolean // 加载中
icon: string // 图标类名
onClick: JSFunction // 点击事件
}
```
### 输入框 (TinyInput)
```typescript
interface TinyInputProps {
modelValue: string | number;
type: 'text' | 'number' | 'password' | 'textarea';
placeholder: string;
disabled: boolean;
readonly: boolean;
clearable: boolean; // 可清空
size: 'medium' | 'small' | 'mini';
maxlength: number;
onChange: JSExpression; // ⚠️ 使用 JSExpression 引用方法
onFocus: JSExpression;
onBlur: JSExpression;
onKeyup: JSExpression; // 键盘事件 (如 Enter 键处理)
}
// ⚠️ 双向绑定示例:
"modelValue": {
"type": "JSExpression",
"value": "this.state.inputText",
"model": true // 标准双向绑定 (v-model)
}
// ⚠️ 事件绑定示例:
"onChange": {
"type": "JSExpression",
"value": "this.handleInputChange"
}
// methods 中定义:
"handleInputChange": {
"type": "JSFunction",
"value": "function(event) { this.state.inputText = event; }"
}
// Enter 键处理示例:
"onKeyup": {
"type": "JSExpression",
"value": "this.handleInputKeyup"
}
// methods 中定义:
"handleInputKeyup": {
"type": "JSFunction",
"value": "function(event) { if (event.keyCode === 13) { this.submitForm(event); } }"
}
```
### 表格 (TinyGrid)
```typescript
interface TinyGridProps {
data: Array<any> // 表格数据
columns: Array<{
// 列配置
field: string
title: string
width?: number
fixed?: 'left' | 'right'
align?: 'left' | 'center' | 'right'
editor?: {
component: string
type?: 'visible' | 'default'
}
}>
border: boolean
stripe: boolean // 斑马纹
height: string | number
autoResize: boolean
}
```
### 对话框 (TinyDialogBox)
```typescript
interface TinyDialogBoxProps {
visible: boolean // 是否显示
title: string
width: string
fullscreen: boolean
top: string
modal: boolean
lockScroll: boolean
beforeClose: JSFunction
onClose: JSFunction
}
```
### 选择器 (TinySelect)
```typescript
interface TinySelectProps {
modelValue: string | number | Array<any>
multiple: boolean
disabled: boolean
clearable: boolean
placeholder: string
options: Array<{
label: string
value: any
disabled?: boolean
}>
remote: boolean // 远程搜索
remoteMethod: JSFunction
onChange: JSFunction
}
```
### 标签页 (TinyTabs)
```typescript
interface TinyTabsProps {
activeName: string
type: '' | 'card' | 'border-card'
tabPosition: 'top' | 'right' | 'bottom' | 'left'
stretch: boolean
onTabClick: JSFunction
}
// 子项 TinyTabItem
interface TinyTabItemProps {
title: string
name: string
disabled: boolean
}
```
### 表单 (TinyForm)
```typescript
interface TinyFormProps {
modelValue: Record<string, any>;
rules: Record<string, any>; // 校验规则
labelWidth: string;
labelPosition: 'left' | 'right' | 'top';
inline: boolean;
disabled: boolean;
validate: JSExpression;
resetFields: JSExpression;
}
// ⚠️ 表单双向绑定示例:
"modelValue": {
"type": "JSExpression",
"value": "this.state.formData",
"model": true // 标准双向绑定 (v-model)
}
// ⚠️ 表单内输入框双向绑定:
"modelValue": {
"type": "JSExpression",
"value": "this.state.formData.name",
"model": true // 标准双向绑定用 true{prop} 仅用于具名 v-model (v-model:xxx)
}
// 表单验证示例:
"methods": {
"handleSubmit": {
"type": "JSFunction",
"value": "async function(event) { const valid = await this.$refs.formRef.validate(); if (valid) { /* 提交表单 */ } }"
}
}
// 表单项 TinyFormItem
interface TinyFormItemProps {
label: string;
prop: string;
required: boolean;
rules: Array<any>;
}
```
### 布局组件
#### 容器 (div)
```typescript
interface DivProps {
className: string
style: string
}
```
#### Span (span)
```typescript
interface SpanProps {
className: string
style: string
}
```
#### 图标 (Icon)
```typescript
interface IconProps {
name: string // 图标名称,如 "IconChevronLeft"
style: string
className: string
}
```
#### 文本 (Text)
```typescript
interface TextProps {
text: string | II18n // 支持i18n
style: string
className: string
}
```
### 数据展示
#### 树形控件 (TinyTree)
```typescript
interface TinyTreeProps {
data: Array<{
label: string
children?: Array<any>
id: string | number
}>
showCheckbox: boolean
checkOnClickNode: boolean
defaultExpandAll: boolean
filterNodeMethod: JSFunction
onCheckChange: JSFunction
onNodeClick: JSFunction
}
```
#### 分页 (TinyPager)
```typescript
interface TinyPagerProps {
currentPage: number
pageSizes: Array<number>
pageSize: number
total: number
layout: string // 如 "total, sizes, prev, pager, next, jumper"
onCurrentChange: JSFunction
onSizeChange: JSFunction
}
```
#### 进度条 (TinyProgress)
```typescript
interface TinyProgressProps {
percentage: number // 0-100
type: 'line' | 'circle' | 'dashboard'
status: 'success' | 'exception' | 'warning'
strokeWidth: number
color: string | string[]
}
```
### 开关和选择
#### 开关 (TinySwitch)
```typescript
interface TinySwitchProps {
modelValue: boolean
disabled: boolean
width: number
activeText: string
inactiveText: string
onChange: JSFunction
}
```
#### 单选框 (TinyRadio)
```typescript
interface TinyRadioProps {
modelValue: string | number | boolean
label: string | number
disabled: boolean
border: boolean
onChange: JSFunction
}
// 单选组 TinyRadioGroup
interface TinyRadioGroupProps {
modelValue: any
size: 'medium' | 'small' | 'mini'
fill: string
textColor: string
onChange: JSFunction
}
```
#### 复选框 (TinyCheckbox)
```typescript
interface TinyCheckboxProps {
modelValue: boolean | string | number
label: string
trueLabel: string
falseLabel: string
disabled: boolean
border: boolean
onChange: JSFunction
}
// 复选组 TinyCheckboxGroup
interface TinyCheckboxGroupProps {
modelValue: Array<any>
size: string
min: number
max: number
onChange: JSFunction
}
```
#### 日期选择 (TinyDatePicker)
```typescript
interface TinyDatePickerProps {
modelValue: string | Date
type: 'year' | 'month' | 'date' | 'dates' | 'week' | 'datetime' | 'datetimerange' | 'daterange'
placeholder: string
startPlaceholder: string
endPlaceholder: string
format: string
disabled: boolean
clearable: boolean
onChange: JSFunction
}
```
### 消息提示
#### 警告 (TinyAlert)
```typescript
interface TinyAlertProps {
title: string
type: 'success' | 'warning' | 'info' | 'error'
description: string
closable: boolean
center: boolean
closeText: string
showIcon: boolean
}
```
#### 消息 (TinyModal)
```typescript
// 使用方法
{
"type": "JSResource",
"value": "this.$modal.message({ message: '操作成功', status: 'success' })"
}
```
#### 确认框 (TinyConfirm)
```typescript
// 使用方法
{
"type": "JSResource",
"value": "this.$modal.confirm({ message: '确定删除?' }).then(() => {})"
}
```
## 图表组件
### 折线图 (TinyLineChart)
```typescript
interface TinyLineChartProps {
data: Array<any>
settings: {
dimensions?: string[]
metrics?: string[]
xAxisType?: 'category' | 'value' | 'time'
yAxisType?: 'category' | 'value' | 'time'
yAxisName?: string[]
area?: boolean
}
}
```
### 柱状图 (TinyBarChart)
```typescript
interface TinyBarChartProps {
data: Array<any>
settings: {
dimensions?: string[]
metrics?: string[]
axisSite?: { top?: string[] }
label?: { show?: boolean }
}
}
```
### 饼图 (TinyPieChart)
```typescript
interface TinyPieChartProps {
data: Array<any>
settings: {
dimension?: string
metrics?: string[]
radius?: number | number[]
}
}
```
## 组件查询
不要手动 grep bundle.json≈1MB。用查询脚本
```bash
node scripts/query_components.mjs props TinyButton # 某组件的属性表(模糊匹配)
node scripts/query_components.mjs list # 全部组件
node scripts/query_components.mjs cat 表单 # 某分类下的组件
node scripts/query_components.mjs search 表格 # 全字段搜索
```
## 常见问题与规则
通用规则(事件绑定、双向绑定 `model`、`occupier`、`className`、生命周期、参数传递)的精简表见 [SKILL.md](../SKILL.md)「Critical Rules / Troubleshooting」完整 ❌/✅ 示例与 TS 接口见 [protocol.md](protocol.md);条件渲染 / 循环渲染等模式见 [patterns.md](patterns.md)。本文件专注于各组件的 props/events 参考。

File diff suppressed because it is too large Load Diff

View File

@ -1,595 +0,0 @@
# TinyEngine DSL Protocol Reference
本文档包含 TinyEngine 低代码平台 DSL 协议的完整参考,用于生成符合协议规范的 JSON 数据。
## 目录
1. [应用协议](#应用协议)
2. [页面结构](#页面结构)
3. [组件结构](#组件结构)
4. [区块结构](#区块结构)
5. [保留字](#保留字)
6. [插槽语法](#插槽语法)
7. [数据源](#数据源)
8. [国际化](#国际化)
---
## 应用协议
### 应用结构
```typescript
interface IAppSchema {
version: string // 协议版本号,如 "1.0.0"
componentsMap: IComponentMap[] // 组件映射关系
componentsTree: IPageSchema[] // 应用包含的页面列表
bridge: IBridge[] // 桥接源(工具函数、依赖)
meta: IAppMeta // 应用基础信息
dataSource?: IDataSource // 应用级数据源
i18n?: II18n // 应用级国际化
utils?: any[] // 工具类
constants?: Record<string, any> // 常量
css?: string // 全局CSS
config?: IAppConfig // 应用配置
}
interface IComponentMap {
componentName: string // 渲染时使用的组件名
package: string // npm包名
version: string // 版本号
destructuring: boolean // 是否解构
exportName: string // 导出名
subName?: string // 子导出名
}
interface IAppMeta {
appId: string | number // App Schema 中建议使用整数 (e.g., 918);服务端持久化时会转成字符串
name: string
description: string
creator: string
git_group?: string
project_name?: string
gmt_create: string
gmt_modified: string
}
/**
* App ID 格式建议:
* - App Schema 文件中的 `id` 字段: 整数类型 (e.g., 918)
* - App Schema 文件中的 `meta.appId` 字段: 整数类型 (e.g., 918)
* - App Metadata 文件中的 `id` 字段: 整数类型 (e.g., 918)
* - Page 文件中的 `app` 字段: 字符串类型 (e.g., "918")
*
* 注意: pages.js 使用 appId.toString() 查询页面,直接落盘的 Page 文件必须用字符串 app 引用。
*/
interface IAppConfig {
sdkVersion: string
historyMode: 'hash' | 'browser'
targetRootID: string
}
interface IBridge {
name: string
type: 'npm' | 'function'
content?: {
package?: string
version?: string
exportName?: string
subName?: string
destructuring?: boolean
main?: string
}
}
```
---
## 页面结构
### 页面 Schema
```typescript
interface IPageSchema {
componentName: 'Page' // 固定值
fileName: string // 页面文件名
meta: IPageMeta // 页面元信息
props?: IProps // 页面属性
state?: Record<string, any> // 页面状态
methods?: Record<string, IJSFunction> // 页面方法
lifeCycles?: Record<string, IJSFunction> // 生命周期
children?: IComponentSchema[] | string // 子组件
css?: string // 页面CSS (换行必须转义为 \n)
dataSource?: IDataSource // 页面数据源
utils?: any[] // 页面工具函数
bridge?: IBridge[] // 页面桥接源
occupier?: null | IOccupier // ⚠️ 必须为 null 才能编辑页面
}
interface IPageMeta {
id: number
title: string
description?: string
router: string // 不能以 / 开头,不支持路由参数 xxx/:id
creator: string
isHome: boolean
parentId: string // 顶层时为 "0"
rootElement: string // 如 "div"
group: string // 如 "staticPages"
gmt_create: string
gmt_modified: string
}
interface IOccupier {
id: number
username: string
email: string
is_admin: boolean
}
```
### 页面示例
```json
{
"componentName": "Page",
"fileName": "HomePage",
"meta": {
"id": 1,
"title": "首页",
"router": "home",
"creator": "admin",
"isHome": true,
"parentId": "0",
"rootElement": "div",
"group": "staticPages",
"description": "应用首页",
"gmt_create": "2024-01-01 00:00:00",
"gmt_modified": "2024-01-01 00:00:00"
},
"props": {},
"state": {
"count": 0,
"message": "Hello"
},
"methods": {
"handleClick": {
"type": "JSFunction",
"value": "function(event) { this.state.count++ }"
}
},
"lifeCycles": {
"onMounted": {
"type": "JSFunction",
"value": "function onMounted() { console.log('Page mounted'); }"
}
},
"css": ".container { padding: 20px; }",
"occupier": null,
"children": []
}
```
### 生命周期钩子
可用的生命周期名称(必须以 `on` 开头):
| 生命周期 | 说明 | Vue 等价 |
| ----------------- | ------------------- | ----------------- |
| `setup` | 组合式 API 设置入口 | setup() |
| `onBeforeMount` | 挂载前 | onBeforeMount() |
| `onMounted` | 挂载后 | onMounted() |
| `onBeforeUpdate` | 更新前 | onBeforeUpdate() |
| `onUpdated` | 更新后 | onUpdated() |
| `onBeforeUnmount` | 卸载前 | onBeforeUnmount() |
| `onUnmounted` | 卸载后 | onUnmounted() |
**setup 生命周期特殊参数**:
```json
{
"lifeCycles": {
"setup": {
"type": "JSFunction",
"value": "function setup({ props, state, watch, onMounted, onUpdated }) {\n // 使用这些参数进行响应式编程\n watch(() => props.data, (newVal) => { console.log('Data changed:', newVal); });\n}"
}
}
}
```
---
## 组件结构
### 组件 Schema
```typescript
interface IComponentSchema {
componentName: string // 组件名或区块名
componentType?: 'block' // 为区块时设置此值
id: string // 唯一ID
props?: IProps // 组件属性
children?: IComponentSchema[] | string // 子组件
condition?: ICondition // 条件渲染
}
interface IProps {
[key: string]: IPropValue | any
}
type IPropValue = string | number | boolean | Array<any> | Object | IJSExpression | II18n | IJSFunction | IJSResource
interface IJSExpression {
type: 'JSExpression'
value: string // 如 "this.state.count"
model?: boolean | { prop: string } // 双向绑定: true 表示 v-model{prop:"xxx"} 表示 v-model:xxx
params?: string[] // 事件附加参数 (追加在 event 之后)
}
// ⚠️ 事件绑定规则 (CRITICAL - 常见错误区域):
// 1. 事件绑定必须使用 JSExpression不能用 JSFunction
// 2. 引用 methods 中定义的方法
// 3. 第一个参数自动是 eventparams 中的参数追加在后面
// 4. ❌ 禁止JSExpression 的 value 中包含 function 定义
// 示例:
// ✅ 正确 - 绑定: "onClick": { "type": "JSExpression", "value": "this.handleClick", "params": ["'id'"] }
// ❌ 错误 - 绑定: "onClick": { "type": "JSExpression", "value": "function(event) { ... }" }
// 调用: handleClick(event, 'id')
// 方法定义: "handleClick": { "type": "JSFunction", "value": "function(event, id) { ... }" }
//
// 记忆口诀:
// - JSExpression = 引用 (this.methodName)
// - JSFunction = 定义 (function() {...})
// - 事件绑定用引用 (JSExpression),方法定义用函数 (JSFunction)
// ⚠️ 双向绑定规则:
// 1. 标准双向绑定使用 model: true
// "modelValue": { "type": "JSExpression", "value": "this.state.text", "model": true }
// 等价于 Vue: v-model="state.text"
// 2. 具名双向绑定使用 model: { "prop": "xxx" }
// "visible": { "type": "JSExpression", "value": "this.state.visible", "model": { "prop": "visible" } }
// 等价于 Vue: v-model:visible="state.visible"
interface II18n {
type: 'i18n'
key: string // 国际化key
}
interface IJSFunction {
type: 'JSFunction'
value: string // 函数字符串
}
interface IJSResource {
type: 'JSResource'
value: string // 如 "this.utils.formatDate()"
}
interface ICondition {
type: 'JSExpression'
value: string // 条件表达式
}
```
### 属性类型说明
| 类型 | 说明 | 示例 |
| -------------- | ---------- | -------------------------------------------------------- |
| 字面值 | 直接值 | `"text"`, `123`, `true` |
| `JSExpression` | 表达式绑定 | `{"type": "JSExpression", "value": "this.state.count"}` |
| `i18n` | 国际化 | `{"type": "i18n", "key": "app.title"}` |
| `JSFunction` | 函数 | `{"type": "JSFunction", "value": "function() {}"}` |
| `JSResource` | 资源引用 | `{"type": "JSResource", "value": "this.utils.format()"}` |
### ⚠️ 常见错误:事件绑定中的类型混淆
**错误示例** (在事件绑定中使用 `JSExpression``value` 中包含函数定义):
```json
// ❌ 错误
{
"componentName": "TinyButton",
"props": {
"onClick": {
"type": "JSExpression",
"value": "function(event) { this.doSomething(); }"
}
}
}
```
**正确做法** (将函数定义放在 `methods` 中,事件绑定引用方法):
```json
// ✅ 正确
{
"methods": {
"handleClick": {
"type": "JSFunction",
"value": "function(event) { this.doSomething(); }"
}
},
"children": [
{
"componentName": "TinyButton",
"props": {
"onClick": {
"type": "JSExpression",
"value": "this.handleClick"
}
}
}
]
}
```
**关键规则**:
- `JSExpression.value` = 方法引用 (如 `this.methodName`)
- `JSFunction.value` = 函数定义 (如 `function() {...}`)
- 事件绑定用 `JSExpression`,函数定义用 `JSFunction`
---
## 区块结构
### 区块 Schema
```typescript
interface IBlockSchema {
componentName: 'Block' // 固定值
fileName: string // 区块文件名
label: string // 区块HTML标签
css?: string // 区块CSS
props?: IProps // 区块属性(可配置)
state?: Record<string, any> // 区块状态
methods?: Record<string, IJSFunction> // 区块方法
lifeCycles?: Record<string, IJSFunction> // 生命周期
schema: IBlockSchemaConfig // 区块对外暴露的配置schema
children?: IComponentSchema[] // 区块内容
dataSource?: IDataSource // 区块数据源
}
interface IBlockSchemaConfig {
properties: IPropertyConfig[] // 可配置属性
events?: Record<string, IEventConfig> // 可触发事件
slots?: Record<string, any> // 插槽定义
}
interface IPropertyConfig {
label: { zh_CN: string }
description?: { zh_CN: string }
content: IPropertyItem[]
}
interface IPropertyItem {
property: string // 属性名
type: string | string[] // 属性类型
defaultValue: any // 默认值
label: { text: { zh_CN: string } }
widget: {
// 配置组件
component: string
props?: any
}
required?: boolean
cols?: number
}
```
### 区块使用示例
在页面中引用区块:
```json
{
"componentName": "MyBlock", // 区块的fileName
"componentType": "block",
"id": "block-001",
"props": {
"title": "标题", // 传递给区块的props
"data": {
"type": "JSExpression",
"value": "this.state.list"
}
}
}
```
---
## 保留字
以下`componentName`为保留关键字,不允许使用同名物料:
| ComponentName | 说明 | 用途 |
| ------------- | -------------------- | ------------------------------------- |
| `Page` | 页面容器 | 配合`fileName`确定页面名称 |
| `Block` | 区块容器 | 配合`fileName`确定区块名称 |
| `Component` | 业务组件容器(预留) | - |
| `Template` | 虚拟容器,不渲染 | 用于具名插槽children 为[]时出码跳过 |
| `Slot` | 插槽定义 | 定义具名插槽 |
| `Collection` | 数据源容器,不渲染 | 提供数据源,出码跳过 |
| `Text` | 文本节点 | 使用 span 渲染text 属性包含内容 |
---
## 插槽语法
### 定义插槽
```json
{
"componentName": "slot",
"props": {
"name": "formSlot"
},
"children": [
{
"componentName": "tiny-input",
"props": {}
}
]
}
```
生成代码:`<slot><tiny-input></tiny-input></slot>`
### 使用作用域插槽
```json
{
"componentName": "template",
"props": {
"slot": {
"name": "footer",
"params": ["row"]
}
},
"children": [...]
}
```
生成代码:`<template #footer="{ row }">...</template>`
### 表格插槽示例
```json
{
"slots": {
"header": {
"type": "JSSlot",
"params": ["column"],
"value": [
{
"componentName": "div",
"children": [...]
}
]
}
}
}
```
---
## 数据源
### 数据源结构
```typescript
interface IDataSource {
dataHandler?: string
list: IDataSourceItem[]
}
interface IDataSourceItem {
id: string | number
name: string
desc?: string
app: string
type: 'fetch' | 'value'
data?: {
columns?: Array<any>
data?: Array<any>
dataHandler?: IJSFunction
errorHandler?: IJSFunction
option?: {
method: string
url: string
}
shouldFetch?: IJSFunction
willFetch?: IJSFunction
}
value?: {
data?: Array<any>
columns?: Array<any>
}
}
```
### Collection 容器使用数据源
```json
{
"componentName": "collection",
"props": {
"dataSource": "tableData"
},
"children": [
{
"componentName": "tiny-grid",
"props": {
"data": {
"type": "JSExpression",
"value": "this.tableData"
},
"columns": [...]
}
}
]
}
```
---
## 国际化
### i18n 结构
```typescript
interface II18n {
[locale: string]: {
[key: string]: string // key-value对支持模板如 "Hello ${name}"
}
}
```
### i18n 示例
```json
{
"i18n": {
"zh-CN": {
"app-title": "我的应用",
"welcome": "你好 ${name}"
},
"en-US": {
"app-title": "My App",
"welcome": "Hello ${name}"
}
}
}
```
### 使用 i18n
在 props 中:
```json
{
"props": {
"text": {
"type": "i18n",
"key": "app-title"
}
}
}
```
带参数:
```json
{
"props": {
"text": {
"type": "i18n",
"key": "welcome",
"params": {
"name": "World"
}
}
}
}
```

View File

@ -1,179 +0,0 @@
#!/usr/bin/env node
/**
* TinyEngine CSS Syntax Checker
*
* 检查DSL中的CSS字段是否有语法错误基础模式括号匹配基本语法无需额外依赖
*
* 零依赖仅用 Node 标准库
* tinycss2 / postcss 模式依赖外部环境已精简basic 是默认且为编排脚本使用的模式
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
/** 普通对象判定(非 null、非数组 */
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
* 解包外层包装结构返回内层 DSL页面在 page_content 区块在 content
* 仅当内层确实是含 componentName 的节点时才解包否则原样返回
*/
function extractInnerDsl(dslData) {
if (isPlainObject(dslData)) {
for (const key of ['page_content', 'content']) {
const inner = dslData[key];
if (isPlainObject(inner) && 'componentName' in inner) {
return inner;
}
}
}
return dslData;
}
/** CSS 语法检查器(基础模式,无需额外依赖) */
export class BasicCssChecker {
/** @param {*} dslData */
constructor(dslData) {
this.dsl = dslData;
this.errors = [];
this.warnings = [];
}
/** 检查 CSS 语法 */
check() {
// 从外层包装(page_content/content)解包到内层 DSL 后再读取 css
const inner = extractInnerDsl(this.dsl);
const cssString = inner.css ?? '';
if (!cssString) {
this.warnings.push('No CSS field found');
return true;
}
return this._checkCss(cssString);
}
/** 基础检查:括号匹配、基本语法 */
_checkCss(css) {
// 检查括号匹配
const stack = [];
for (let i = 0; i < css.length; i++) {
const char = css[i];
if (char === '{') {
stack.push([char, i]);
} else if (char === '}') {
if (stack.length === 0 || stack[stack.length - 1][0] !== '{') {
this.errors.push(`Unmatched '}' at position ${i}`);
return false;
}
stack.pop();
} else if (char === '(') {
stack.push([char, i]);
} else if (char === ')') {
if (stack.length === 0 || stack[stack.length - 1][0] !== '(') {
this.errors.push(`Unmatched ')' at position ${i}`);
return false;
}
stack.pop();
}
}
if (stack.length) {
for (const [char, pos] of stack) {
this.errors.push(`Unclosed '${char}' at position ${pos}`);
}
return false;
}
// 移除注释进行检查
const cssNoComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
// 检查是否有 CSS 规则
if (!cssNoComments.includes('{')) {
this.warnings.push('CSS may not contain any rules');
}
// 检查分号使用
const rules = [...cssNoComments.matchAll(/\{([^}]*)\}/g)].map((m) => m[1]);
for (const rule of rules) {
const properties = rule.split(';');
// 最后一个可能为空
for (const propRaw of properties.slice(0, -1)) {
const prop = propRaw.trim();
if (prop && !prop.includes(':')) {
this.warnings.push(`Property without colon: ${prop.slice(0, 50)}`);
}
}
}
return this.errors.length === 0;
}
/** 生成报告 */
report() {
const lines = [];
if (this.errors.length) {
lines.push('❌ CSS Errors:');
for (const error of this.errors) lines.push(` - ${error}`);
}
if (this.warnings.length) {
lines.push('⚠️ CSS Warnings:');
for (const warning of this.warnings) lines.push(` - ${warning}`);
}
if (this.errors.length === 0 && this.warnings.length === 0) {
lines.push('✅ CSS check passed!');
}
return lines.join('\n');
}
}
// 可用模式表(仅保留 basic
const CHECKERS = { basic: BasicCssChecker };
function main() {
if (process.argv.length < 3) {
console.log('Usage: check_css.mjs <dsl-file> [mode]');
console.log(' mode: basic (default)');
process.exit(1);
}
const filePath = process.argv[2];
const mode = process.argv[3] || 'basic';
// 读取 DSL 文件
let dslData;
try {
const text = fs.readFileSync(filePath, 'utf8');
dslData = JSON.parse(text);
} catch (e) {
if (e instanceof SyntaxError) {
console.log(`❌ Invalid JSON: ${e.message}`);
} else if (e.code === 'ENOENT') {
console.log(`❌ File not found: ${filePath}`);
} else {
throw e;
}
process.exit(1);
}
// 选择检查器
const CheckerClass = CHECKERS[mode];
if (!CheckerClass) {
console.log(`❌ Unknown mode: ${mode}`);
console.log(`Available modes: ${Object.keys(CHECKERS).join(', ')}`);
process.exit(1);
}
const checker = new CheckerClass(dslData);
const isValid = checker.check();
console.log(checker.report());
process.exit(isValid ? 0 : 1);
}
const __filename = fileURLToPath(import.meta.url);
if (path.resolve(process.argv[1] || '') === __filename) {
main();
}

View File

@ -1,185 +0,0 @@
#!/usr/bin/env node
/**
* TinyEngine Event Binding Checker
*
* 检查DSL文件中的事件绑定是否正确使用JSExpression引用方法
* 而不是在value中直接写函数定义
*
* 零依赖仅用 Node 标准库
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
/** 普通对象判定(非 null、非数组 */
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
* 解包外层包装结构返回内层 DSL
* 落盘的页面/区块文件是"外层包装 + 内层 DSL"
* - 页面 DSL page_content
* - 区块 DSL content
* 仅当内层确实是含 componentName 的节点时才解包避免误吞同名普通字段否则原样返回
*/
function extractInnerDsl(dslData) {
if (isPlainObject(dslData)) {
for (const key of ['page_content', 'content']) {
const inner = dslData[key];
if (isPlainObject(inner) && 'componentName' in inner) {
return inner;
}
}
}
return dslData;
}
const EVENT_KEYS = [
'onClick', 'onChange', 'onKeyup', 'onKeyDown', 'onKeyPress',
'onFocus', 'onBlur', 'onSubmit', 'onInput', 'onTabClick',
'onCurrentChange', 'onSizeChange', 'onCheckChange',
'onNodeClick', 'onRowClick', 'onCellClick',
];
export class EventBindingChecker {
/** @param {*} dslData */
constructor(dslData) {
this.dsl = dslData;
this.errors = [];
this.warnings = [];
}
/** 检查所有事件绑定 */
check() {
// 从外层包装(page_content/content)解包到内层 DSL 后再检查
const inner = extractInnerDsl(this.dsl);
this._checkNode(inner);
return this.errors.length === 0;
}
/** 递归检查节点 */
_checkNode(node) {
if (isPlainObject(node)) {
// 检查当前节点的事件绑定
this._checkEventBindings(node);
// 递归检查子节点(仅当 children 是数组;字符串子节点无需处理)
if (Array.isArray(node.children)) {
for (const child of node.children) {
this._checkNode(child);
}
}
} else if (Array.isArray(node)) {
for (const item of node) {
this._checkNode(item);
}
}
}
/** 检查单个节点的事件绑定(含 props 内的事件绑定) */
_checkEventBindings(node) {
const component = Object.prototype.hasOwnProperty.call(node, 'componentName') ? node.componentName : 'unknown';
// 两处都需要校验,避免漏检 props 内的事件。
this._checkEventHolder(node, component);
if (isPlainObject(node.props)) {
this._checkEventHolder(node.props, component);
}
}
/** 检查某个属性容器(节点本身或其 props内的事件绑定 */
_checkEventHolder(holder, component) {
// 检查所有可能的事件属性
for (const key of EVENT_KEYS) {
if (key in holder) {
const value = holder[key];
if (isPlainObject(value)) {
this._checkEventValue(component, key, value);
}
}
}
// 也检查以 'on' 开头的属性
for (const [key, value] of Object.entries(holder)) {
if (key.startsWith('on') && !EVENT_KEYS.includes(key)) {
if (isPlainObject(value)) {
this._checkEventValue(component, key, value);
}
}
}
}
/** 检查事件值 */
_checkEventValue(component, eventKey, value) {
const valueType = value.type;
const valueContent = Object.prototype.hasOwnProperty.call(value, 'value') ? value.value : '';
// 错误1: 使用 JSFunction 类型进行事件绑定
if (valueType === 'JSFunction') {
this.errors.push(
`${component}.${eventKey}: 使用了JSFunction类型应该使用JSExpression引用methods中的方法`
);
}
// 错误2: JSExpression 的 value 中包含函数定义
if (valueType === 'JSExpression' && typeof valueContent === 'string' && valueContent.startsWith('function')) {
this.errors.push(
`${component}.${eventKey}: JSExpression的value中包含函数定义 '${valueContent.slice(0, 30)}...'` +
`应该引用方法如 'this.methodName'`
);
}
}
/** 生成报告 */
report() {
const lines = [];
if (this.errors.length) {
lines.push('❌ 发现事件绑定错误:');
for (const error of this.errors) lines.push(` - ${error}`);
}
if (this.warnings.length) {
lines.push('⚠️ 警告:');
for (const warning of this.warnings) lines.push(` - ${warning}`);
}
if (this.errors.length === 0 && this.warnings.length === 0) {
lines.push('✅ 所有事件绑定检查通过!');
}
return lines.join('\n');
}
}
function main() {
if (process.argv.length < 3) {
console.log('Usage: check_event_bindings.mjs <dsl-file>');
process.exit(1);
}
const filePath = process.argv[2];
let dslData;
try {
const text = fs.readFileSync(filePath, 'utf8');
dslData = JSON.parse(text);
} catch (e) {
if (e instanceof SyntaxError) {
console.log(`❌ Invalid JSON: ${e.message}`);
} else if (e.code === 'ENOENT') {
console.log(`❌ File not found: ${filePath}`);
} else {
throw e;
}
process.exit(1);
}
const checker = new EventBindingChecker(dslData);
const isValid = checker.check();
console.log(checker.report());
process.exit(isValid ? 0 : 1);
}
const __filename = fileURLToPath(import.meta.url);
if (path.resolve(process.argv[1] || '') === __filename) {
main();
}

View File

@ -1,215 +0,0 @@
#!/usr/bin/env node
/**
* TinyEngine Component Catalog Query
*
* designer-demo/public/mock/bundle.json 查询组件元数据
* 避免把 ~1MB 全量清单加载进上下文零依赖仅用 Node 标准库
*
* 用法:
* node query_components.mjs list 列出全部组件 / 中文名 / 分类 / 描述
* node query_components.mjs categories 列出所有分类及组件数
* node query_components.mjs cat <分类关键字> 列出某分类下的组件
* node query_components.mjs props <组件名或中文> 查某组件的属性表支持模糊匹配
* node query_components.mjs search <关键字> 按名 / 中文名 / 描述 / 分类搜索
*
* 可用 BUNDLE_JSON=<path> 环境变量覆盖 bundle.json 位置
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const SCRIPT_DIR = path.dirname(__filename);
// scripts/ → tinyengine-dsl-generator → skills → .agents → 仓库根
const REPO_ROOT = path.resolve(SCRIPT_DIR, '..', '..', '..', '..');
const DEFAULT_BUNDLE = path.join(REPO_ROOT, 'designer-demo', 'public', 'mock', 'bundle.json');
function loadComponents() {
const file = process.env.BUNDLE_JSON || DEFAULT_BUNDLE;
let json;
try {
json = JSON.parse(fs.readFileSync(file, 'utf8'));
} catch (e) {
if (e.code === 'ENOENT') {
console.error(`❌ 找不到 bundle.json: ${file}(可用 BUNDLE_JSON=<path> 覆盖)`);
} else if (e instanceof SyntaxError) {
console.error(`❌ bundle.json 解析失败: ${e.message}`);
} else {
throw e;
}
process.exit(1);
}
return json?.data?.materials?.components ?? [];
}
/** 取一个节点的中文标签,兼容 label.text.zh_CN / label.zh_CN / name.zh_CN */
function zhLabel(node) {
if (!node) return '';
return node.text?.zh_CN || node.label?.text?.zh_CN || node.label?.zh_CN || node.name?.zh_CN || '';
}
/** 组件名(强制字符串,少数条目 component 字段非字符串) */
function cname(c) {
return String(c?.component ?? '');
}
function truncate(s, n = 46) {
s = (s || '').replace(/\s+/g, ' ').trim();
return s.length > n ? s.slice(0, n - 1) + '…' : s;
}
function pad(s, n) {
s = String(s ?? '');
return s.length >= n ? s : s + ' '.repeat(n - s.length);
}
function listAll(comps) {
console.log(`${pad('componentName', 22)} ${pad('中文名', 12)} ${pad('category', 12)} description`);
console.log('-'.repeat(86));
for (const c of comps) {
console.log(`${pad(c.component, 22)} ${pad(zhLabel(c), 12)} ${pad(c.category || '', 12)} ${truncate(c.description)}`);
}
console.log(`\n${comps.length} 个组件`);
}
function categories(comps) {
const m = new Map();
for (const c of comps) {
const k = c.category || '(无分类)';
m.set(k, (m.get(k) || 0) + 1);
}
for (const [k, n] of [...m].sort((a, b) => b[1] - a[1])) {
console.log(`${pad(k, 16)} ${n}`);
}
}
function byCategory(comps, cat) {
const hits = comps.filter((c) => (c.category || '').toLowerCase().includes(cat.toLowerCase()));
if (hits.length === 0) {
console.log(`没有匹配分类 "${cat}" 的组件。运行 "categories" 查看全部分类。`);
return;
}
console.log(`分类匹配 "${cat}"${hits.length} 个):`);
for (const c of hits) console.log(` ${pad(c.component, 22)} ${pad(zhLabel(c), 12)} ${truncate(c.description)}`);
}
/** 精确 → 模糊componentName / 中文名)匹配,返回单个 / 多个 / 未命中 */
function findComp(comps, name) {
const exact = comps.find((c) => cname(c) === name);
if (exact) return { comp: exact };
const lower = name.toLowerCase();
const byName = comps.filter((c) => cname(c).toLowerCase().includes(lower));
if (byName.length === 1) return { comp: byName[0] };
const byZh = comps.filter((c) => zhLabel(c).includes(name));
if (byZh.length === 1) return { comp: byZh[0] };
const pooled = new Map();
for (const c of [...byName, ...byZh]) pooled.set(cname(c), c);
if (pooled.size > 0) return { multiple: [...pooled.values()] };
return {};
}
function showProps(comp) {
console.log(`${comp.component}${zhLabel(comp)}(分类: ${comp.category || '?'}`);
if (comp.description) console.log(comp.description);
if (comp.npm?.package) {
const imp = comp.npm.destructuring ? `{ ${comp.npm.exportName} }` : comp.npm.exportName;
console.log(`npm: ${comp.npm.package} ${imp}`);
}
console.log('');
const groups = comp.schema?.properties || [];
if (groups.length === 0) {
console.log('(该组件无 props schema');
return;
}
console.log(`${pad('property', 18)}${pad('widget', 22)}${pad('required', 9)}label / description`);
console.log('-'.repeat(86));
let count = 0;
for (const g of groups) {
for (const item of g.content || []) {
const prop = item.property || '?';
const widget = item.widget?.component || '';
const req = item.required ? 'required' : '';
const desc = truncate(item.description?.zh_CN || zhLabel(item), 40);
console.log(`${pad(prop, 18)}${pad(widget, 22)}${pad(req, 9)}${desc}`);
count++;
}
}
console.log(`\n${count} 个属性`);
}
function search(comps, kw) {
const lower = kw.toLowerCase();
const hits = comps.filter(
(c) =>
cname(c).toLowerCase().includes(lower) ||
zhLabel(c).includes(kw) ||
(c.description || '').toLowerCase().includes(lower) ||
(c.category || '').toLowerCase().includes(lower)
);
if (hits.length === 0) {
console.log(`没有匹配 "${kw}" 的组件。`);
return;
}
console.log(`匹配 "${kw}"${hits.length} 个):`);
for (const c of hits) console.log(` ${pad(c.component, 22)} ${pad(zhLabel(c), 12)} ${pad(c.category || '', 10)} ${truncate(c.description, 34)}`);
}
function usage() {
console.log(`TinyEngine 组件查询
用法:
node query_components.mjs list 列出全部组件
node query_components.mjs categories 列出分类
node query_components.mjs cat <分类> 某分类下的组件
node query_components.mjs props <组件名|中文> 某组件的属性表模糊匹配
node query_components.mjs search <关键字> 全字段搜索
示例:
node query_components.mjs props TinyButton
node query_components.mjs props button
node query_components.mjs cat 表单`);
}
function main() {
const [cmd, ...rest] = process.argv.slice(2);
const comps = loadComponents();
switch (cmd) {
case undefined:
case '--help':
case '-h':
return usage();
case 'list':
return listAll(comps);
case 'categories':
case 'cats':
return categories(comps);
case 'cat':
if (!rest[0]) return usage();
return byCategory(comps, rest.join(' '));
case 'props':
if (!rest[0]) return usage();
{
const r = findComp(comps, rest.join(' '));
if (r.comp) return showProps(r.comp);
if (r.multiple) {
console.log(`"${rest.join(' ')}" 匹配到多个组件,请指定更精确的名字:`);
for (const c of r.multiple) console.log(` ${pad(c.component, 22)} ${zhLabel(c)}`);
return;
}
console.log(`未找到组件 "${rest.join(' ')}"。运行 "list" 查看全部,或用 "search <关键字>"。`);
}
return;
case 'search':
if (!rest[0]) return usage();
return search(comps, rest.join(' '));
default:
console.error(`未知命令: ${cmd}`);
return usage();
}
}
main();

View File

@ -1,43 +0,0 @@
#!/bin/bash
# TinyEngine DSL 综合验证脚本
# 运行所有检查结构验证、事件绑定检查、CSS检查
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DSL_FILE="$1"
if [ -z "$DSL_FILE" ]; then
echo "Usage: validate_all.sh <dsl-file>"
exit 1
fi
if [ ! -f "$DSL_FILE" ]; then
echo "❌ File not found: $DSL_FILE"
exit 1
fi
echo "======================================"
echo "TinyEngine DSL 综合验证"
echo "文件: $DSL_FILE"
echo "======================================"
echo
# 1. 结构验证
echo "1⃣ 结构验证..."
node "$SCRIPT_DIR/validate_dsl.mjs" "$DSL_FILE" || exit 1
echo
# 2. 事件绑定检查
echo "2⃣ 事件绑定检查..."
node "$SCRIPT_DIR/check_event_bindings.mjs" "$DSL_FILE" || exit 1
echo
# 3. CSS 语法检查
echo "3⃣ CSS 语法检查..."
node "$SCRIPT_DIR/check_css.mjs" "$DSL_FILE" basic || exit 1
echo
echo "======================================"
echo "✅ 所有验证通过!"
echo "======================================"

View File

@ -1,306 +0,0 @@
#!/usr/bin/env node
/**
* TinyEngine DSL Validator
*
* 验证生成的DSL是否符合TinyEngine协议规范
*
* 零依赖仅用 Node 标准库
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
/** 把 JS 值映射为类型名,用于告警里的 "got: <类型>" 描述。 */
function typeName(value) {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
switch (typeof value) {
case 'string':
return 'string';
case 'boolean':
return 'boolean';
case 'number':
return Number.isInteger(value) ? 'integer' : 'number';
default:
return 'object';
}
}
/** 普通对象判定(非 null、非数组 */
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export class TinyEngineValidator {
/**
* @param {*} dslData DSL 数据
* @param {string} [schemaType='auto'] 'page' | 'block' | 'app' | 'auto'
*/
constructor(dslData, schemaType = 'auto') {
this.original = dslData;
// 落盘文件是"外层包装 + 内层 DSL",这里解包到内层 DSL 再做协议校验。
const [dsl, fromWrapper] = TinyEngineValidator._unwrap(dslData);
this.dsl = dsl;
this._fromWrapper = fromWrapper;
this.schemaType = schemaType;
this.errors = [];
this.warnings = [];
// 自动检测 schema 类型
if (schemaType === 'auto') {
this.schemaType = this._detectSchemaType();
}
}
/**
* 识别并解包外层包装结构
* TinyEngine 落盘的页面/区块文件是"外层包装 + 内层 DSL"结构
* - 页面真正的页面 DSL page_content
* - 区块真正的区块 DSL content
* 返回 [内层DSL, 是否来自包装]若不是包装结构原样返回 [原数据, false]
* 仅当内层确实是含 componentName 的节点时才解包避免误吞同名普通字段
*/
static _unwrap(dslData) {
if (isPlainObject(dslData)) {
for (const key of ['page_content', 'content']) {
const inner = dslData[key];
if (isPlainObject(inner) && 'componentName' in inner) {
return [inner, true];
}
}
}
return [dslData, false];
}
/** 自动检测 schema 类型 */
_detectSchemaType() {
if ('componentName' in this.dsl) {
const cn = this.dsl.componentName;
if (cn === 'Page') return 'page';
if (cn === 'Block') return 'block';
// 其它 componentName 落到 unknown不进入 app 分支)
} else if ('componentsTree' in this.dsl || 'version' in this.dsl) {
return 'app';
}
return 'unknown';
}
/** 验证 DSL返回是否有效 */
validate() {
if (this.schemaType === 'page') return this._validatePage();
if (this.schemaType === 'block') return this._validateBlock();
if (this.schemaType === 'app') return this._validateApp();
this.errors.push(`Unknown schema type: ${this.schemaType}`);
return false;
}
_validatePage() {
for (const field of ['componentName', 'fileName']) {
if (!(field in this.dsl)) this.errors.push(`Missing required field: ${field}`);
}
if (this.dsl.componentName !== 'Page') {
this.errors.push(`Page componentName must be 'Page', got: ${this.dsl.componentName}`);
}
// Page 外层包装的 app 引用必须是字符串pages.js list()/create() 均用 appId.toString()
// 查询;写成数字会导致直接落盘的 page 文件查不到。
if (this._fromWrapper && isPlainObject(this.original)) {
const appRef = this.original.app;
if (appRef !== undefined && typeof appRef !== 'string') {
this.warnings.push(
`Page wrapper 'app' should be string, got: ${typeName(appRef)} (pages.js queries with appId.toString(); numeric app ref won't be found by list())`
);
}
}
// 原始页面协议(IPageSchema)要求 meta但外层包装格式把 meta 等元信息上提到包装层,
// page_content 内不再含 meta。因此仅在校验"裸"页面 DSL 时强制要求 meta。
if ('meta' in this.dsl) {
this._validateMeta(this.dsl.meta);
} else if (!this._fromWrapper) {
this.errors.push('Missing required field: meta');
}
if (this.dsl.children) {
this._validateChildren(this.dsl.children);
}
return this.errors.length === 0;
}
_validateBlock() {
for (const field of ['componentName', 'fileName']) {
if (!(field in this.dsl)) this.errors.push(`Missing required field: ${field}`);
}
if (this.dsl.componentName !== 'Block') {
this.errors.push(`Block componentName must be 'Block', got: ${this.dsl.componentName}`);
}
if ('schema' in this.dsl) {
this._validateBlockSchema(this.dsl.schema);
}
if (this.dsl.children) {
this._validateChildren(this.dsl.children);
}
return this.errors.length === 0;
}
_validateApp() {
for (const field of ['version', 'componentsMap', 'componentsTree']) {
if (!(field in this.dsl)) this.errors.push(`Missing required field: ${field}`);
}
// 验证 app ID 格式
if ('id' in this.dsl) {
const rootId = this.dsl.id;
if (!Number.isInteger(rootId)) {
this.warnings.push(`App Schema 'id' should be integer, got: ${typeName(rootId)} (will be coerced)`);
}
}
// 验证 meta.appId 格式
if ('meta' in this.dsl && 'appId' in this.dsl.meta) {
const appId = this.dsl.meta.appId;
if (!Number.isInteger(appId)) {
this.warnings.push(`meta.appId should be integer, got: ${typeName(appId)} (will be coerced)`);
}
}
// 验证 componentsMap
if ('componentsMap' in this.dsl) {
this._validateComponentsMap(this.dsl.componentsMap);
}
// 验证 componentsTree
if ('componentsTree' in this.dsl) {
for (const page of this.dsl.componentsTree) {
const pageValidator = new TinyEngineValidator(page, 'auto');
if (!pageValidator.validate()) {
const fileName = Object.prototype.hasOwnProperty.call(page, 'fileName') ? page.fileName : '?';
for (const e of pageValidator.errors) {
this.errors.push(`[Page ${fileName}] ${e}`);
}
}
}
}
return this.errors.length === 0;
}
_validateMeta(meta) {
for (const field of ['id', 'title', 'router', 'creator', 'isHome', 'parentId', 'rootElement']) {
if (!(field in meta)) this.errors.push(`Missing meta field: ${field}`);
}
}
_validateComponentsMap(componentsMap) {
for (const comp of componentsMap) {
for (const field of ['componentName', 'package', 'exportName']) {
if (!(field in comp)) this.errors.push(`componentsMap missing field: ${field}`);
}
}
}
_validateBlockSchema(schema) {
if ('properties' in schema) {
for (const prop of schema.properties) {
if (!('content' in prop)) this.errors.push("Block schema property missing 'content'");
}
}
}
_validateChildren(children) {
// children 可以是字符串(文本子节点),按协议(IComponentSchema[] | string)
// 这是合法形态,无需逐项校验;递归调用遇到字符串时同样直接返回。
if (typeof children === 'string') return;
children.forEach((child, i) => {
if (!isPlainObject(child)) {
this.errors.push(`Child at index ${i} is not an object`);
return;
}
if (!('componentName' in child)) {
this.errors.push(`Child at index ${i} missing componentName`);
}
// 检查 ID
if (!('id' in child)) {
this.warnings.push(`Child at index ${i} missing id (recommended)`);
}
// 检查 props 中是否有错误的 'class' 字段(应该是 'className'
if ('props' in child && isPlainObject(child.props)) {
if ('class' in child.props) {
const component = Object.prototype.hasOwnProperty.call(child, 'componentName') ? child.componentName : 'unknown';
this.errors.push(
`${component} at index ${i} uses 'class' in props, should use 'className' instead (React/Vue convention)`
);
}
}
// 验证嵌套 children
if (child.children) {
this._validateChildren(child.children);
}
});
}
/** 生成验证报告 */
report() {
const lines = [];
if (this.errors.length) {
lines.push('❌ Validation Errors:');
for (const error of this.errors) lines.push(` - ${error}`);
}
if (this.warnings.length) {
lines.push('⚠️ Warnings:');
for (const warning of this.warnings) lines.push(` - ${warning}`);
}
if (this.errors.length === 0 && this.warnings.length === 0) {
lines.push('✅ Validation passed!');
}
return lines.join('\n');
}
}
function main() {
if (process.argv.length < 3) {
console.log('Usage: validate_dsl.mjs <dsl-file> [schema-type]');
console.log(' schema-type: page, block, app, or auto (default)');
process.exit(1);
}
const filePath = process.argv[2];
const schemaType = process.argv[3] || 'auto';
let dslData;
try {
const text = fs.readFileSync(filePath, 'utf8');
dslData = JSON.parse(text);
} catch (e) {
if (e instanceof SyntaxError) {
console.log(`❌ Invalid JSON: ${e.message}`);
} else if (e.code === 'ENOENT') {
console.log(`❌ File not found: ${filePath}`);
} else {
throw e;
}
process.exit(1);
}
const validator = new TinyEngineValidator(dslData, schemaType);
const isValid = validator.validate();
console.log(validator.report());
process.exit(isValid ? 0 : 1);
}
const __filename = fileURLToPath(import.meta.url);
if (path.resolve(process.argv[1] || '') === __filename) {
main();
}

View File

@ -1,220 +0,0 @@
#!/usr/bin/env node
/**
* TinyEngine Page DSL 综合验证
*
* 验证包装格式的页面DSL文件包含 name, id, app, route, page_content 等字段
* 运行所有检查结构验证事件绑定检查CSS检查
*
* 零依赖仅用 Node 标准库
* 通过子进程调用同目录下的 check_event_bindings.mjs / check_css.mjs子进程直接继承 stdout/stderr
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
/** 普通对象判定(非 null、非数组 */
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/** 把 JS 值映射为类型名,用于告警里的 "got: <类型>" 描述。 */
function typeName(value) {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
switch (typeof value) {
case 'string':
return 'string';
case 'boolean':
return 'boolean';
case 'number':
return Number.isInteger(value) ? 'integer' : 'number';
default:
return 'object';
}
}
/** 读取并解析 JSON 文件;失败时抛出 SyntaxErrorJSON或带 code 的系统错误(文件)。 */
function readJson(filePath) {
const text = fs.readFileSync(filePath, 'utf8');
return JSON.parse(text);
}
/** 验证包装格式的页面文件 */
function validatePageWrapper(filePath) {
console.log(`验证文件: ${filePath}`);
console.log('='.repeat(50));
let data;
try {
data = readJson(filePath);
} catch (e) {
if (e instanceof SyntaxError) {
console.log(`❌ Invalid JSON: ${e.message}`);
} else if (e.code === 'ENOENT') {
console.log(`❌ File not found: ${filePath}`);
} else {
throw e;
}
return false;
}
// 检查是否是包装格式
if (!('page_content' in data)) {
console.log('❌ 不是有效的页面文件(缺少 page_content 字段)');
return false;
}
const pageContent = data.page_content;
// 检查必要字段
for (const field of ['name', 'id', 'app', 'route']) {
if (!(field in data)) {
console.log(`❌ 缺少必要字段: ${field}`);
return false;
}
}
// 验证 app 字段格式:页面外层 app 引用必须是字符串。
// pages.js list()/create() 均用 appId.toString() 查询,数字 app 会导致直接落盘的页面查不到。
if ('app' in data) {
const appField = data.app;
if (typeof appField !== 'string') {
console.log(
`⚠️ WARNING: 'app' field should be string, got: ${typeName(appField)} (pages.js queries with appId.toString(); numeric app ref won't be found by list())`
);
}
}
// 检查 page_content 中的必要字段
if (!('componentName' in pageContent)) {
console.log('❌ page_content 缺少 componentName 字段');
return false;
}
if (pageContent.componentName !== 'Page') {
console.log(`❌ componentName 必须是 'Page',实际是: ${pageContent.componentName}`);
return false;
}
if (!('fileName' in pageContent)) {
console.log('❌ page_content 缺少 fileName 字段');
return false;
}
console.log('✅ 包装格式检查通过');
return true;
}
/** 运行所有检查器 */
function runCheckers(filePath) {
// 1. 事件绑定检查
console.log('\n1⃣ 事件绑定检查...');
const eventResult = spawnSync(
process.execPath,
[path.join(SCRIPT_DIR, 'check_event_bindings.mjs'), filePath],
{ stdio: 'inherit' }
);
if (eventResult.status !== 0) return false;
// 2. CSS 检查
console.log('\n2⃣ CSS 语法检查...');
const cssResult = spawnSync(
process.execPath,
[path.join(SCRIPT_DIR, 'check_css.mjs'), filePath, 'basic'],
{ stdio: 'inherit' }
);
if (cssResult.status !== 0) return false;
return true;
}
/** 检查是否正确使用 className 而不是 class */
function checkClassNameUsage(filePath) {
let data;
try {
data = readJson(filePath);
} catch (e) {
// JSON 语法错误或文件读取问题(如 ENOENT由其他检查负责报告
// 此处不掩盖其他意外运行错误。
if (e instanceof SyntaxError || (e && typeof e.code === 'string')) {
return true;
}
console.log(`❌ className 检查异常: ${e}`);
return false;
}
const pageContent = Object.prototype.hasOwnProperty.call(data, 'page_content')
? data.page_content
: {};
const errors = [];
const checkNode = (node) => {
if (isPlainObject(node)) {
// 检查 props 中是否有 'class'
if ('props' in node && isPlainObject(node.props)) {
if ('class' in node.props) {
const component = Object.prototype.hasOwnProperty.call(node, 'componentName')
? node.componentName
: 'unknown';
errors.push(`${component} 使用了 'class' 而不是 'className'`);
}
}
// 递归检查 children
if (Array.isArray(node.children)) {
for (const child of node.children) checkNode(child);
}
} else if (Array.isArray(node)) {
for (const item of node) checkNode(item);
}
};
checkNode(pageContent);
if (errors.length) {
console.log("\n❌ 发现错误的 'class' 使用(应该使用 'className':");
for (const error of errors) console.log(` - ${error}`);
return false;
}
console.log('\n✅ className 检查通过');
return true;
}
function main() {
if (process.argv.length < 3) {
console.log('Usage: validate_page.mjs <page-file>');
console.log('验证包装格式的TinyEngine页面DSL文件');
process.exit(1);
}
const filePath = process.argv[2];
// 运行所有检查
let allPassed = true;
// 1. 包装格式检查
if (!validatePageWrapper(filePath)) allPassed = false;
// 2. className 检查
if (!checkClassNameUsage(filePath)) allPassed = false;
// 3. 运行其他检查器
if (!runCheckers(filePath)) allPassed = false;
// 总结
console.log('\n' + '='.repeat(50));
if (allPassed) {
console.log('✅ 所有验证通过!');
} else {
console.log('❌ 验证失败,请修复错误后重试');
}
process.exit(allPassed ? 0 : 1);
}
main();

View File

@ -8,4 +8,4 @@ tool_params:
secsolar:
source_dir: ./
cmetrics:
exclude: vite.config.js|package.json|index.js|axios.js|.eslintrc.js|mockServer|packages/engine-cli/template|packages/vue-generator/test|packages/vue-generator/src/templates|packages/build/vite-plugin-meta-comments/src/test
exclude: vite.config.js|package.json|index.js|mockServer/assets|packages/vue-generator/test

View File

@ -1,7 +0,0 @@
SQL_HOST=localhost
SQL_PORT=3306
SQL_USER=root
SQL_PASSWORD=admin
SQL_DATABASE=tiny_engine
backend_url=http://localhost:9090/material-center/api/component/bundle/create

9
.eslintignore Normal file
View File

@ -0,0 +1,9 @@
.vscode
dist
public
package-lock.json
**/node_modules/**
tmp
temp
mockServer
packages/vue-generator/**/output/**

31
.eslintrc.js Normal file
View File

@ -0,0 +1,31 @@
module.exports = {
env: {
browser: true,
es2015: true,
worker: true,
node: true,
jest: true
},
extends: ['eslint:recommended', 'plugin:vue/vue3-essential'],
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@babel/eslint-parser',
ecmaVersion: 'latest',
sourceType: 'module',
requireConfigFile: false,
babelOptions: {
parserOpts: {
plugins: ['jsx']
}
}
},
plugins: ['vue'],
rules: {
'no-console': 'error',
'no-debugger': 'error',
'space-before-function-paren': 'off',
'vue/multi-word-component-names': 'off',
'no-use-before-define': 'error',
'no-unused-vars': ['error', { ignoreRestSiblings: true, varsIgnorePattern: '^_', argsIgnorePattern: '^_' }]
}
}

View File

@ -1,56 +0,0 @@
version: v1
labels:
- label: 'ignore-for-release'
sync: true
matcher:
title: '^[vV]?\d+(\.\d+){2}.*'
body: '#ignore'
- label: 'breaking-change'
sync: true
matcher:
title: '^.+!:|#breaking'
body: '## Does this PR introduce a breaking change\?\s+- \[[xX]\] *Yes'
- label: 'enhancement'
sync: true
matcher:
title: '^[fF]eat.*'
- label: 'bug'
sync: true
matcher:
title: '^[fF]ix.*'
- label: 'documentation'
sync: true
matcher:
title: '^[dD](ocs|ocumentation).*'
files: 'docs/**/*.md'
- label: 'refactoring'
sync: true
matcher:
title: '^[rR]efactor.*'
- label: 'test'
sync: true
matcher:
title: '^[tT]est.*'
files: '**/__tests__/**'
- label: 'chore'
sync: true
matcher:
title: '^[cC]hore.*'
- label: 'ci'
sync: true
matcher:
title: '^[cC]i.*'
files: '.github/**'
- label: 'ospp'
sync: true
matcher:
baseBranch: '^ospp-\d+\/.*'
- label: '1.x'
sync: true
matcher:
baseBranch: '^v1\.x'
- label: 'release'
sync: true
matcher:
baseBranch: '^release\/v2.*'

11
.github/release.yml vendored
View File

@ -2,6 +2,8 @@ changelog:
exclude:
labels:
- ignore-for-release
authors:
- allcontributors[bot]
categories:
- title: Breaking Changes 🛠
labels:
@ -16,16 +18,9 @@ changelog:
labels:
- Semver-Patch
- bug
- title: "📖 Documentation"
- title: Other Changes
labels:
- documentation
- title: "🔧 Maintenance"
labels:
- refactoring
- test
- unit-test
- chore
- ci
- title: "Other Changes"
labels: ["*"]
collapse: true

View File

@ -1,76 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
if: github.repository == 'opentiny/tiny-engine'
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install
- name: Run Build
run: pnpm run build:plugin && pnpm run build:alpha > /tmp/build-alpha.log 2>&1
- name: Upload build logs
uses: actions/upload-artifact@v4
with:
name: build-alpha-log
path: /tmp/build-alpha.log
- name: Parse Publish tag
id: parse_tag
run: |
tag_name="${GITHUB_REF#refs/tags/}"
if [[ "$tag_name" == *alpha* ]]; then
echo "dist_tag=alpha" >> "$GITHUB_OUTPUT"
elif [[ "$tag_name" == *beta* ]]; then
echo "dist_tag=beta" >> "$GITHUB_OUTPUT"
elif [[ "$tag_name" == *rc* ]]; then
echo "dist_tag=rc" >> "$GITHUB_OUTPUT"
else
echo "dist_tag=latest" >> "$GITHUB_OUTPUT"
fi
- name: Verify clean working directory
run: |
if [[ -n "$(git status --porcelain)" ]]; then
echo "Working directory is not clean"
exit 1
fi
- name: Verify package version match tag
run: |
tag_name="${GITHUB_REF#refs/tags/}"
package_version=$(pnpm lerna list --scope=@opentiny/tiny-engine --json | jq -r '.[0].version')
if [[ "$tag_name" != "v$package_version" ]]; then
echo "Tag name $tag_name does not match package version $package_version"
exit 1
fi
- name: Publish package to npm
run: pnpm lerna publish from-package --dist-tag ${{steps.parse_tag.outputs.dist_tag}} --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,19 +0,0 @@
name: Pull Request Auto Labeler
on:
pull_request_target:
types: [opened, edited]
permissions:
# Setting up permissions in the workflow to limit the scope of what it can do. Optional!
contents: read # the config file
pull-requests: write # for labeling pull requests (on: pull_request_target or on: pull_request)
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: fuxingloh/multi-labeler@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }} # optional, default to '${{ github.token }}'
config-path: .github/auto-labeler.yml # optional, default to '.github/labeler.yml'

View File

@ -1,162 +0,0 @@
name: Deploy to CDN
on:
workflow_dispatch:
inputs:
is_latest_release:
description: '当前分支是否是最新release版本'
required: true
default: true
type: boolean
jobs:
check-secrets:
runs-on: ubuntu-latest
outputs:
secrets-ready: ${{ steps.check.outputs.secrets-ready }}
steps:
- name: Check required secrets
id: check
run: |
if [[ -z "${{ secrets.HUAWEI_CLOUD_AK }}" ]] || \
[[ -z "${{ secrets.HUAWEI_CLOUD_SK }}" ]] || \
[[ -z "${{ secrets.HUAWEI_CLOUD_ENDPOINT }}" ]] || \
[[ -z "${{ secrets.HUAWEI_CLOUD_BUCKET }}" ]]; then
echo "secrets-ready=false" >> $GITHUB_OUTPUT
echo "::error::Required Huawei Cloud secrets are not configured."
echo "::error::Please set: HUAWEI_CLOUD_AK, HUAWEI_CLOUD_SK, HUAWEI_CLOUD_ENDPOINT, HUAWEI_CLOUD_BUCKET"
exit 1
fi
echo "secrets-ready=true" >> $GITHUB_OUTPUT
echo "✅ All required secrets are configured"
build:
runs-on: ubuntu-latest
needs: check-secrets
outputs:
version-timestamp: ${{ steps.prepare-version.outputs.version_timestamp }}
cdn-base: ${{ steps.prepare-version.outputs.cdn_base }}
cdn-base-latest: ${{ steps.prepare-version.outputs.cdn_base_latest }}
obs-path: ${{ steps.prepare-version.outputs.obs_path }}
obs-path-latest: ${{ steps.prepare-version.outputs.obs_path_latest }}
concurrency:
group: deploy-cdn
cancel-in-progress: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install
- name: add environment variable
run: |
VITE_ORIGIN_URL="https://agent.opentiny.design/"
cat <<EOF >> designer-demo/env/.env.alpha
# ---- appended by CI (deploy-cdn) ----
VITE_ORIGIN=$VITE_ORIGIN_URL
EOF
echo "VITE_ORIGIN_URL=$VITE_ORIGIN_URL"
- id: prepare-version
name: Prepare version-timestamp
run: |
# Extract version from package.json
VERSION=$(node -p "require('./designer-demo/package.json').version")
# Generate timestamp in YYYYMMDD-HHMMSS format
TIMESTAMP=$(TZ="Asia/Shanghai" date +%Y%m%d-%H%M%S)
# Combine for version-timestamp
VERSION_TIMESTAMP="${VERSION}-${TIMESTAMP}"
# Set CDN base path
CDN_BASE="https://res-static.opentiny.design/tiny-engine-designer/${VERSION_TIMESTAMP}/"
CDN_BASE_LATEST="https://res-static.opentiny.design/tiny-engine-designer/latest/"
OBS_PATH="tiny-engine-designer/${VERSION_TIMESTAMP}/"
OBS_PATH_LATEST="tiny-engine-designer/latest"
# Export as environment variables for subsequent steps
echo "VERSION_TIMESTAMP=$VERSION_TIMESTAMP" >> $GITHUB_ENV
echo "CDN_BASE=$CDN_BASE" >> $GITHUB_ENV
echo "OBS_PATH=$OBS_PATH" >> $GITHUB_ENV
# Set outputs for job-level export
echo "version_timestamp=$VERSION_TIMESTAMP" >> $GITHUB_OUTPUT
echo "cdn_base=$CDN_BASE" >> $GITHUB_OUTPUT
echo "cdn_base_latest=$CDN_BASE_LATEST" >> $GITHUB_OUTPUT
echo "obs_path=$OBS_PATH" >> $GITHUB_OUTPUT
echo "obs_path_latest=$OBS_PATH_LATEST" >> $GITHUB_OUTPUT
echo "Version-Timestamp: $VERSION_TIMESTAMP"
echo "CDN Base: $CDN_BASE"
echo "OBS Path: $OBS_PATH"
- name: Run Build
run: |
set -eo pipefail
pnpm run build:plugin 2>&1 | tee /tmp/build-plugin.log
# Run build:alpha equivalent with --base parameter
pnpm run build:alpha --base=${{ env.CDN_BASE }} 2>&1 | tee /tmp/build-alpha.log
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: designer-demo-dist
path: ./designer-demo/dist/
retention-days: 1
deploy-cdn:
runs-on: ubuntu-latest
needs: [check-secrets, build]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: designer-demo-dist
path: ./designer-demo/dist/
- name: Install obsutil
run: |
curl -o obsutil.tar.gz https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_linux_amd64.tar.gz
tar -xzf obsutil.tar.gz
chmod +x obsutil_linux_amd64_*/obsutil
sudo mv obsutil_linux_amd64_*/obsutil /usr/local/bin/obsutil
- name: Configure and Upload to OBS
run: |
obsutil config -i=${{ secrets.HUAWEI_CLOUD_AK }} \
-k=${{ secrets.HUAWEI_CLOUD_SK }} \
-e=${{ secrets.HUAWEI_CLOUD_ENDPOINT }}
# Upload to versioned path
obsutil cp ./designer-demo/dist \
obs://${{ secrets.HUAWEI_CLOUD_BUCKET }}/${{ needs.build.outputs.obs-path }} \
-r -f -flat
# If is_latest_release is true, also upload to latest path
if [ "${{ github.event.inputs.is_latest_release }}" = "true" ]; then
# use cdn-base-latest replace cdn-base in all ./designer-demo/dist files
find ./designer-demo/dist -type f \( -name "*.html" -o -name "*.js" -o -name "*.mjs" -o -name "*.css" \) \
-exec sed -i "s|${{ needs.build.outputs.cdn-base }}|${{ needs.build.outputs.cdn-base-latest }}|g" {} +
obsutil cp ./designer-demo/dist \
obs://${{ secrets.HUAWEI_CLOUD_BUCKET }}/${{ needs.build.outputs.obs-path-latest }} \
-r -f -flat
fi
echo "Uploaded to: obs://${{ secrets.HUAWEI_CLOUD_BUCKET }}/${{ needs.build.outputs.obs-path }}"
echo "CDN URL: https://res-static.opentiny.design/${{ needs.build.outputs.obs-path }}"

View File

@ -1,74 +0,0 @@
name: Deploy to GitHub Pages
on:
push:
branches: [develop]
workflow_dispatch:
inputs:
backend_url:
description: 'Backend Base URL (e.g. https://agent.bytedev.site/)'
required: true
default: 'https://agent.bytedev.site/'
type: string
jobs:
deploy-gh-pages:
runs-on: ubuntu-latest
concurrency:
group: deploy-gh-pages
cancel-in-progress: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install
- name: add environment variable
env:
ENV_BACKEND_URL: ${{ github.event.inputs.backend_url || 'https://agent.bytedev.site/' }}
run: |
if [[ "$ENV_BACKEND_URL" == *$'\n'* || "$ENV_BACKEND_URL" == *$'\r'* ]]; then
echo "::error::backend_url must be a single-line value"
exit 1
fi
if [[ ! "$ENV_BACKEND_URL" =~ ^https?://[a-zA-Z0-9._-]+(:[0-9]+)?(/[a-zA-Z0-9._/~:%-]*)?$ ]]; then
echo "::error::backend_url must be an http(s) base URL using only URL-safe characters (letters, digits, . _ - / ~ : %)"
exit 1
fi
ENV_BACKEND_URL="${ENV_BACKEND_URL%/}/"
cat <<EOF >> designer-demo/env/.env.alpha
# ---- appended by CI (gh-pages) ----
VITE_ORIGIN=$ENV_BACKEND_URL
EOF
echo "VITE_ORIGIN_URL=$ENV_BACKEND_URL"
- name: Run Build
run: |
set -eo pipefail
pnpm run build:plugin 2>&1 | tee /tmp/build-plugin.log
pnpm run build:alpha 2>&1 | tee /tmp/build-alpha.log
- name: Deploy
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./designer-demo/dist/
keep_files: true
force_orphan: false
user_name: 'github-actions[bot]'
user_email: 'github-actions[bot]@users.noreply.github.com'

View File

@ -4,40 +4,33 @@ on:
push:
branches: []
pull_request:
branches: [develop, main, refactor/develop, release/*]
branches: [develop,main]
jobs:
push-check:
runs-on: ubuntu-latest # windows-latest || macos-latest
runs-on: ubuntu-latest # windows-latest || macos-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 18
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm i
- name: Install dependencies
run: pnpm i
- name: Get changed files
id: get_changed_files
uses: tj-actions/changed-files@v41
with:
files: |
**.js
**.vue
**.jsx
- name: Run ESLint
run: npx eslint ${{steps.get_changed_files.outputs.all_changed_files}}
- name: Run Build
run: pnpm run build:plugin && pnpm run build:alpha > build-alpha.log 2>&1
- name: Upload build logs
uses: actions/upload-artifact@v4
with:
name: build-alpha-log
path: build-alpha.log
- name: Get changed files
id: get_changed_files
uses: tj-actions/changed-files@v41
with:
files: |
**.js
**.vue
**.jsx
- name: Run ESLint
run: npx eslint ${{steps.get_changed_files.outputs.all_changed_files}}
- name: Run Build
run: pnpm run build:plugin && pnpm run build:alpha

5
.gitignore vendored
View File

@ -6,7 +6,6 @@ yarn.lock
pnpm-lock.yaml
lerna-debug.log
packages/design-core/bundle-deps
designer-demo/bundle-deps
# local env files
.env.local
@ -28,7 +27,3 @@ pnpm-debug.log*
*.sw?
tmp
temp
__pycache__
# .claude/skills is a generated link to .agents/skills (see scripts/link-skills.js)
.claude/skills

0
.husky/pre-commit Executable file → Normal file
View File

View File

@ -1,5 +1,3 @@
dist
package-lock.json
**/node_modules/**
# 忽略该文件夹下的测试对比文件,防止自动去掉分号之后导致测试失败
packages/build/vite-plugin-meta-comments/test/expected/**
**/node_modules/**

View File

@ -1,7 +1,5 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 120,
"trailingComma": "none",
"endOfLine": "auto"
}
semi: false
singleQuote: true
printWidth: 120
trailingComma: 'none'
endOfLine: 'auto'

120
AGENTS.md
View File

@ -1,120 +0,0 @@
# TinyEngine — Repository Instructions for Coding Agents
## Purpose and Scope
This file is the canonical source of truth for repo-wide agent instructions.
- Applies to the whole repository unless a closer `AGENTS.md` overrides it for a subtree.
- `CLAUDE.md` is a compatibility entrypoint that imports this file. Do not maintain a second independent copy of the same rules.
- Keep this file limited to repo-wide guidance. Package-specific implementation details belong in package-level instruction files.
## Repository Snapshot
- Monorepo: pnpm workspaces + lerna (independent versioning)
- Primary stack: Vue 3, Vite, JavaScript/TypeScript
- Package manager: `pnpm` only for interactive work in this repo
- Designer app: `designer-demo/`
- Local mock backend: `mockServer/`
## Working Model
- Inspect the affected package, its `package.json`, and the nearest instruction file before editing.
- Keep changes scoped. Do not normalize unrelated files or rename fixtures just for consistency.
- Prefer targeted package-level validation over whole-repo commands when possible.
- Treat `pnpm lint` and `pnpm format` as mutating commands, not read-only verification.
- Do not invoke `npm` or `yarn` directly for normal repo work. Existing package scripts may still shell out internally; leave that alone unless the task is specifically about package scripts.
## Common Commands
### Read-mostly commands
```sh
pnpm install
pnpm dev
pnpm build:plugin
pnpm build:alpha
pnpm --filter @opentiny/tiny-engine-dsl-vue test:unit
```
### Mutating commands
```sh
pnpm lint # ESLint with --fix
pnpm format # Prettier --write
```
Canonical script definitions live in:
- `package.json`
- `packages/*/package.json`
- `.github/workflows/push-check.yml`
- `.github/workflows/Release.yml`
## Verification Matrix
Run the smallest sufficient verification for the change surface, then expand if the change is broad or risky.
1. Docs-only changes:
No code verification required unless the docs change commands or workflow descriptions that should be checked against source files.
2. `packages/vue-generator/**`:
Run the affected testcase or `pnpm --filter @opentiny/tiny-engine-dsl-vue test:unit`.
If generator behavior changes, run the full `test:unit` suite before handoff and inspect any changed `expected/*.vue` files.
3. Published library packages under `packages/**`:
Run the package-local `test` script if one exists.
Run `pnpm build:plugin` when build output or published package behavior may be affected.
4. `designer-demo/**` or shared packages consumed by the demo:
Run `pnpm build:alpha`.
5. Cross-package build or release-facing changes:
Run `pnpm build:plugin` and `pnpm build:alpha`.
6. Config, workspace, CI, or release script changes:
Verify the directly affected command(s) after approval.
## Approval Boundaries
### Always OK
- Read any source file
- Run targeted tests and builds
- Edit implementation files inside existing packages
- Add or update tests that match the scope of the change
- Update docs that reflect current repo behavior
### Ask First
- Changing workspace, lerna, pnpm, ESLint, Prettier, or TypeScript configuration
- Modifying CI workflows, release scripts, or publish flows
- Upgrading major dependencies or changing pinned overrides
- Reordering or adding/removing default vue-generator attribute hooks
- Large-scale edits to generated mappings or vendored patches
When asking first, include:
- what you want to change
- why the current rules or implementation are insufficient
- what verification you would run after approval
### Never
- Use `npm` or `yarn` directly for routine repo commands
- Skip hooks with `--no-verify`
- Hardcode versions for workspace packages
- Edit `patches/` without understanding the upstream issue and the patch purpose
- Rewrite generated expectations or snapshots without validating the new output first
## Task-Specific Expectations
- Bug fix:
Add or update a regression test when behavior changes.
- Refactor:
Preserve behavior and prove it with targeted verification.
- Snapshot or generated output change:
Explain why the output changed and list the affected fixture directories.
- Commit or PR work:
Only do it if asked. Use Conventional Commits and target `develop` unless the user specifies otherwise.
## Gotchas
- `pnpm install` is enforced by `preinstall`; npm and yarn are rejected for direct repo usage.
- `pnpm lint` writes fixes. Use it deliberately.
- CI relies on `build:plugin` and `build:alpha`, not only lint or unit tests.
- Test directories such as `test/`, `expected/`, and `output/` are not always linted; do not treat lint success as fixture validation.

View File

@ -1,7 +0,0 @@
# TinyEngine — Claude Code Entry
This file is intentionally thin. The canonical repo-wide instructions live in `AGENTS.md`.
@./AGENTS.md
When working inside a subtree that has its own `CLAUDE.md`, follow the closer file as an extension of these repo-wide rules.

View File

@ -1,13 +0,0 @@
FROM node:18-alpine as build
WORKDIR /app/
ADD . .
RUN npm config set strict-ssl false \
&& npm config set registry https://registry.npmmirror.com/ \
&& npm install pnpm -g \
&& pnpm i \
&& pnpm build:plugin \
&& pnpm build:prod
FROM nginx:latest
COPY --from=build /app/designer-demo/dist/ /usr/share/nginx/html/

View File

@ -4,9 +4,7 @@
</a>
</p>
<p align="center">TinyEngine enables developers to customize low-code platforms, build low-code platforms online in real time, and support secondary development or integration of low-code platform capabilities.</p>
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/opentiny/tiny-engine)
<p align="center">TinyEngine enables developers to customize low-code platforms, build low-bit platforms online in real time, and support secondary development or integration of low-bit platform capabilities.</p>
English | [简体中文](README.zh-CN.md)
@ -17,54 +15,40 @@ English | [简体中文](README.zh-CN.md)
- Directly generate deployable source code without engine support.
- Allows access to third-party components and customized extension plug-ins.
- Supports high-code and low-code, and hybrid development and deployment of applications.
- The platform accesses LLM capabilities to help developers build applications.
- The platform accesses AI big model capabilities to help developers build applications.
## Documentation
- introhttps://opentiny.design/tiny-engine#/home
- tutorialhttps://opentiny.design/tiny-engine#/help-center/index
- tutorialhttps://opentiny.design/tiny-engine#/help-center/course/engine
- playgroundhttps://opentiny.design/tiny-engine#/tiny-engine-editor
## Usage
## Development
### Environment Setup
- Install Node.js 18+
- Install pnpm 9+
### Dependencies required for installation
```sh
$ npm install -g pnpm
```
### Create Low-Code Platform using CLI
```sh
# Create low-code platform
$ npx @opentiny/tiny-engine-cli@latest create-platform <name>
# Enter the low-code platform directory
$ cd <name>
# Install dependencies
$ pnpm install
```
### Local development: Start the local mock server and use the mock data of the local mock server.
> The mock server included with the created low-code platform only provides basic backend mock functionality. If you need to experience the complete backend service capabilities, please refer to the Java backend startup instructions below.
```sh
$ pnpm dev
```
### Local Development with Java Backend
## Local development, directly connected to the local tiny-engine-webservice server
Java backend repository: https://github.com/opentiny/tiny-engine-backend-java
1. Start <a href="https://github.com/opentiny/tiny-engine-data-center/blob/main/README.md" target="_blank">tiny-engine-data-center</a>
Start Java backend for frontend-backend integration:
2. Start <a href="https://github.com/opentiny/tiny-engine-webservice/blob/main/README.md" target="_blank">tiny-engine-webservice</a>
[Frontend-Backend Integration Documentation](https://docs.opentiny.design/tiny-engine/dev/debugging-of-java-backend)
3. Modify the origin value in `vite.config.js` in the `packages/design-core/` directory of the tiny-engine project to be the address port of your local webService project (the webService port defaults to 7011), such as:
### Materials Synchronization [Solution](https://docs.opentiny.design/tiny-engine/dev/material-sync-solution)
<img alt="Modify port" src="https://res.hc-cdn.com/lowcode-portal/1.1.55/img/docimg/backend_deploy_5.png">
### Materials Synchronization [Solution](https://opentiny.design/tiny-engine#/help-center/course/engine/56)
```sh
$ pnpm splitMaterials
@ -74,7 +58,7 @@ $ pnpm splitMaterials
$ pnpm buildMaterials
```
Open a browser: `http://localhost:8080/?type=app&id=1&tenant=1&pageid=1`
Open a browser: `http://localhost:8080/?type=app&id=918&tenant=1&pageid=NTJ4MjvqoVj8OVsc`
`url search` Parameters:
- `type=app` Application type
@ -85,8 +69,16 @@ Open a browser: `http://localhost:8080/?type=app&id=1&tenant=1&pageid=1`
## Build
```sh
# Build all plug-ins first
pnpm build:plugin
# Build Designer
pnpm run build:alpha or build:prod
pnpm build:alpha or build:prod
```
The folder where the product is located after building
```
tiny-engine/packages/design-core/dist/
```
## Milestones
@ -96,15 +88,10 @@ gantt
dateFormat YYYY-MM-DD
axisFormat %Y-%m-%d
1.0.0-beta.x version : 2023-09-25, 2024-05-20
1.0.0-rc version(refactor version) : 2024-10-01
1.0.0 version : 2024-11-01
2.0.0 version : 2024-12-16
2.1.0 version : 2025-01-02
2.2.0 version : 2025-02-19
2.3.0 version : 2025-03-14
2.4.0 version : 2025-04-07
2.5.0 version : 2025-05-15
1.0.0-beta.x version :active,2023-09-25, 2024-03-31
1.0.0-rc version : 2024-04-01, 2024-06-30
1.0.0 version : 2024-07-01, 2024-07-31
```
## 🤝 Participation and Contribution
@ -116,16 +103,6 @@ Please read the [Contribution Guide](CONTRIBUTING.md) before participating in th
- Add official assistant WeChat opentiny-official and join the technical exchange group
- Join the mailing list opentiny@googlegroups.com
## ❤️ Acknowledgments
Thanks to all the developers who have contributed to TinyEngine!
<p align="center">
<a href="https://github.com/opentiny/tiny-engine/graphs/contributors" target="_blank">
<img alt="Contributors" src="https://contrib.rocks/image?repo=opentiny/tiny-engine">
</a>
</p>
## License
[MIT](LICENSE)
[MIT](LICENSE)

View File

@ -20,49 +20,35 @@
## 文档
- 介绍https://opentiny.design/tiny-engine#/home
- 使用文档https://opentiny.design/tiny-engine#/help-center/index
- 使用文档https://opentiny.design/tiny-engine#/help-center/course/engine
- 演示应用https://opentiny.design/tiny-engine#/tiny-engine-editor
## 使用
## 开发
### 环境准备
- 安装 Node.js 18+
- 安装 pnpm 9+
### 安装所需的依赖
```sh
$ npm install -g pnpm
```
### 使用 cli 创建低代码平台
```sh
# 创建低代码平台
$ npx @opentiny/tiny-engine-cli@latest create-platform <name>
# 进入低代码平台
$ cd <name>
# 安装依赖
$ pnpm install
```
### 本地开发,启动本地 mock 服务器,使用本地 mock 服务器的 mock 数据
> 创建低代码平台后自带的 mock Server 仅包含简单的后端 mock 功能,如果需要体验完整的后端服务能力,请参考下文启动 java 服务端。
```sh
$ pnpm dev
```
### 本地开发,启动 Java 服务端
### 本地开发直连本地的tiny-engine-webservice服务端
java 服务端代码仓库https://github.com/opentiny/tiny-engine-backend-java
1. 启动 <a href="https://github.com/opentiny/tiny-engine-data-center/blob/main/README.md" target="_blank">tiny-engine-data-center</a>
启动 Java 服务端进行前后端联调:
2. 启动 <a href="https://github.com/opentiny/tiny-engine-webservice/blob/main/README.md" target="_blank">tiny-engine-webservice</a>
[前后端联调文档](https://docs.opentiny.design/tiny-engine/dev/debugging-of-java-backend)
3. 修改 tiny-engine 项目 `packages/design-core/` 目录下 `vite.config.js` 中origin的值为自己本地webService项目的地址端口webService端口默认为7011
### 物料同步[方案](https://docs.opentiny.design/tiny-engine/dev/material-sync-solution)
<img alt="修改端口" src="https://res.hc-cdn.com/lowcode-portal/1.1.55/img/docimg/backend_deploy_5.png">
### 物料同步[方案](https://opentiny.design/tiny-engine#/help-center/course/engine/56)
```sh
$ pnpm splitMaterials
@ -72,7 +58,7 @@ $ pnpm splitMaterials
$ pnpm buildMaterials
```
浏览器打开:`http://localhost:8080/?type=app&id=1&tenant=1&pageid=1`
浏览器打开:`http://localhost:8080/?type=app&id=918&tenant=1&pageid=NTJ4MjvqoVj8OVsc`
`url search`参数:
- `type=app` 应用类型
@ -83,26 +69,29 @@ $ pnpm buildMaterials
## 构建
```sh
# 先构建所有插件
pnpm run build:plugin
# 构建设计器
pnpm run build:alpha 或 build:prod
```
构建后产物所在文件夹
```
tiny-engine/packages/design-core/dist/
```
## 里程碑
```mermaid
gantt
gantt
dateFormat YYYY-MM-DD
axisFormat %Y-%m-%d
1.0.0-beta.x version : 2023-09-25, 2024-05-20
1.0.0-rc version(refactor version) : 2024-10-01
1.0.0 version : 2024-11-01
2.0.0 version : 2024-12-16
2.1.0 version : 2025-01-02
2.2.0 version : 2025-02-19
2.3.0 version : 2025-03-14
2.4.0 version : 2025-04-07
2.5.0 version : 2025-05-15
1.0.0-beta.x version :active,2023-09-25, 2024-03-31
1.0.0-rc version : 2024-04-01, 2024-06-30
1.0.0 version : 2024-07-01, 2024-07-31
```
## 🤝 参与贡献
@ -114,16 +103,6 @@ axisFormat %Y-%m-%d
- 添加官方小助手微信 opentiny-official加入技术交流群
- 加入邮件列表 opentiny@googlegroups.com
## ❤️ 致谢
感谢所有为 TinyEngine 做出贡献的开发者们!
<p align="center">
<a href="https://github.com/opentiny/tiny-engine/graphs/contributors" target="_blank">
<img alt="贡献者" src="https://contrib.rocks/image?repo=opentiny/tiny-engine">
</a>
</p>
## 开源协议
[MIT](LICENSE)
[MIT](LICENSE)

View File

@ -1,11 +0,0 @@
export default {
id: 'engine.config',
theme: 'light',
material: ['./mock/bundle.json'],
scripts: [],
styles: [],
// 是否开启 TailWindCSS 特性
enableTailwindCSS: true,
// 是否开启 使用结构化CSS 特性
enableStructuredCss: false
}

View File

@ -1,8 +0,0 @@
# development mode, used by the "vite" command
NODE_ENV=development
VITE_CDN_DOMAIN=https://registry.npmmirror.com
# 使用npmmirror的cdn 时,需要声明 VITE_CDN_TYPE=npmmirror
VITE_CDN_TYPE=npmmirror
# request data via alpha service
# VITE_ORIGIN=

View File

@ -1,10 +0,0 @@
# CDN 本地化配置示例
# 将画布、页面预览需要的 vue、vue-i18n 等等依赖复制到构建产物中
VITE_LOCAL_IMPORT_MAPS=true
# 将本地物料 bundle.json 的 script 和 css 复制到构建产物中
VITE_LOCAL_BUNDLE_DEPS=true
# 将 VITE_LOCAL_BUNDLE_DEPS 复制到构建产物中的目录名称,默认为 local-cdn-static
VITE_LOCAL_IMPORT_PATH=local-cdn-static

View File

@ -1,8 +0,0 @@
# prod mode, used by the "build:prod" script
NODE_ENV=production
# VITE_CDN_DOMAIN=https://unpkg.com
VITE_CDN_DOMAIN=https://registry.npmmirror.com
# 使用npmmirror的cdn 时,需要声明 VITE_CDN_TYPE=npmmirror
VITE_CDN_TYPE=npmmirror
#VITE_ORIGIN=

View File

@ -1,35 +0,0 @@
{
"name": "designer-demo",
"private": true,
"version": "2.11.0",
"type": "module",
"scripts": {
"dev": "cross-env vite",
"dev:withAuth": "cross-env VITE_AUTH=true vite",
"build:alpha": "cross-env NODE_OPTIONS=--max-old-space-size=6144 vite build --mode alpha",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=10240 vite build",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@opentiny/tiny-engine": "workspace:^",
"@opentiny/tiny-engine-meta-register": "workspace:^",
"@opentiny/tiny-engine-utils": "workspace:*",
"@opentiny/vue": "~3.20.0",
"@opentiny/vue-design-smb": "~3.20.0",
"@opentiny/vue-icon": "~3.20.0",
"@opentiny/vue-locale": "~3.20.0",
"@opentiny/vue-renderless": "~3.20.0",
"@opentiny/vue-theme": "~3.20.0",
"@vueuse/core": "^9.6.0",
"vue": "^3.4.21"
},
"devDependencies": {
"@opentiny/tiny-engine-mock": "workspace:^",
"@opentiny/tiny-engine-vite-config": "workspace:^",
"@vitejs/plugin-vue": "^5.1.2",
"cross-env": "^7.0.3",
"vite": "^5.4.2",
"vitest": "3.0.9"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 B

File diff suppressed because it is too large Load Diff

View File

@ -1,62 +0,0 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
// 注册表示例
import { META_SERVICE, META_APP } from '@opentiny/tiny-engine-meta-register'
import engineConfig from './engine.config'
import { HttpService } from './src/composable'
import scriptPlugin from './src/plugins/script'
export default {
[META_SERVICE.Http]: HttpService,
'engine.config': {
...engineConfig
},
// 配置 false 隐藏工具栏清空按钮
[META_APP.Clean]: false,
// 配置 false 隐藏大纲树,手动配置 tree-shaking 为 false仍然不会被 tree-shaking
// #__TINY_ENGINE_TREE_SHAKING__: false
[META_APP.OutlineTree]: false,
// 替换整个页面JS插件手动配置 tree-shaking 为 true
/* #__TINY_ENGINE_TREE_SHAKING__: true */
[META_APP.Page]: scriptPlugin,
// 新增模块
'engine.plugins.custom_id': {
...cuttomPlugin,
id: 'engine.plugins.custom_id'
},
// 调整插件顺序
[META_APP.Layout]: {
options: {
relativeLayoutConfig: {
[META_APP.Script]: {
insertBefore: META_APP.AppManage
},
// 调整插件顺序
[META_APP.Materials]: {
insertAfter: META_APP.State
},
// 调整插件上下位置
[META_APP.Schema]: {
insertBefore: META_APP.Materials
},
// 调整工具栏顺序
[META_APP.Save]: {
insertBefore: META_APP.ThemeSwitch
},
// 支持切换组
[META_APP.Lang]: {
insertAfter: META_APP.Breadcrumb
}
}
}
}
}

View File

@ -1,57 +0,0 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { META_SERVICE, META_APP } from '@opentiny/tiny-engine-meta-register'
import engineConfig from './engine.config'
import { HttpService } from './src/composable'
const baseURL = import.meta.env.BASE_URL || '.'
const baseURLWithoutSlash = baseURL.replace(/\/$/, '')
export default {
[META_SERVICE.Http]: HttpService,
'engine.config': {
...engineConfig
},
// 调整插件顺序示例:
[META_APP.Layout]: {
options: {
relativeLayoutConfig: {
[META_APP.Page]: {
insertBefore: META_APP.State
},
// 调整插件顺序
[META_APP.OutlineTree]: {
insertAfter: META_APP.Materials
},
// 调整插件上下位置
[META_APP.Schema]: {
insertBefore: META_APP.Help
},
// 调整工具栏顺序
[META_APP.Save]: {
insertAfter: META_APP.GenerateCode
},
// 支持切换组
[META_APP.Lang]: {
insertAfter: META_APP.ViewSetting
}
}
}
},
[META_APP.Preview]: {
options: {
// 配置预览跳转的 url根据实际业务需求进行配置
// 文档https://opentiny.design/tiny-engine#/help-center/course/dev/preview-api
previewUrl: ['prod', 'alpha'].includes(import.meta.env.MODE) ? `${baseURLWithoutSlash}/preview.html` : ''
}
}
}

View File

@ -1,136 +0,0 @@
import { createApp } from 'vue'
import { HttpService } from '@opentiny/tiny-engine'
import { useBroadcastChannel } from '@vueuse/core'
import { constants } from '@opentiny/tiny-engine-utils'
import Login from './Login.vue'
const LOGIN_EXPIRED_CODE = 401
const { BROADCAST_CHANNEL } = constants
const { post: globalNotify } = useBroadcastChannel({ name: BROADCAST_CHANNEL.Notify })
const procession = {
promiseLogin: null,
mePromise: {}
}
let loginVM = null
const showError = (url, message) => {
if (message === 'canceled') return // 取消请求场景不报错
globalNotify({
type: 'error',
title: '接口报错',
message: `报错接口: ${url} \n报错信息: ${message ?? ''}`
})
}
const preRequest = (config) => {
const isDevelopEnv = import.meta.env.MODE?.includes('dev')
if (isDevelopEnv && config.url.match(/\/generate\//)) {
config.baseURL = ''
}
const isVsCodeEnv = window.vscodeBridge
if (isVsCodeEnv) {
config.baseURL = ''
}
return config
}
const preResponse = (res) => {
if (res.data?.error) {
showError(res.config?.url, res?.data?.error?.message)
return Promise.reject(res.data.error)
}
return res.data?.data || res.data
}
const openLogin = () => {
if (!window.lowcode) {
const loginDom = document.createElement('div')
document.body.appendChild(loginDom)
loginVM = createApp(Login).mount(loginDom)
window.lowcode = {
platformCenter: {
Session: {
rebuiltCallback: function () {
loginVM.closeLogin()
procession.mePromise.resolve('login ok')
procession.promiseLogin = null
procession.mePromise = {}
}
}
}
}
}
return new Promise((resolve, reject) => {
if (!procession.promiseLogin) {
procession.promiseLogin = loginVM.openLogin(procession, '/api/rebuildSession')
procession.promiseLogin.then((response) => {
HttpService.apis.request(response.config).then(resolve, reject)
})
}
})
}
const errorResponse = (error) => {
// 用户信息失效时,弹窗提示登录
const { response } = error
if (response?.status === LOGIN_EXPIRED_CODE) {
// vscode 插件环境弹出输入框提示登录
if (window.vscodeBridge) {
return Promise.resolve(true)
}
// 浏览器环境弹出小窗登录
if (response?.headers['x-login-url']) {
return openLogin()
}
}
showError(error.config?.url, error?.message)
return response?.data.error ? Promise.reject(response.data.error) : Promise.reject(error.message)
}
const getConfig = (env = import.meta.env) => {
const baseURL = env.VITE_ORIGIN
// 仅在本地开发时,启用 withCredentials
const dev = env.MODE?.includes('dev')
// 获取租户 id
const getTenant = () => new URLSearchParams(location.search).get('tenant')
return {
baseURL,
withCredentials: dev,
headers: {
...(dev && { 'x-lowcode-mode': 'develop' }),
'x-lowcode-org': getTenant()
}
}
}
const customizeHttpService = () => {
const options = {
axiosConfig: getConfig(),
interceptors: {
request: [preRequest],
response: [[preResponse, errorResponse]]
}
}
HttpService.apis.setOptions(options)
return HttpService
}
export default customizeHttpService()

View File

@ -1 +0,0 @@
export { default as HttpService } from './http'

View File

@ -1,63 +0,0 @@
<template>
<span>我是自定义的 input configurator</span>
<tiny-input v-model="value" :type="type" :placeholder="placeholder" :rows="rows" @update:modelValue="change">
</tiny-input>
</template>
<script>
import { ref } from 'vue'
import { Input } from '@opentiny/vue'
export default {
name: 'MyInputConfigurator',
components: {
TinyInput: Input
},
props: {
modelValue: {
type: String
},
type: {
type: String
},
placeholder: {
type: String
},
suffixIcons: {
type: Array,
default: () => []
},
dataType: {
type: String
},
rows: {
type: Number,
default: 10
}
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const value = ref(props.modelValue)
const change = (val) => {
emit('update:modelValue', props.dataType === 'Array' ? val.split(',') : val)
}
return {
value,
change
}
}
}
</script>
<style lang="less" scoped>
.tiny-svg-size {
margin-left: 10px;
font-size: 16px;
&:hover {
cursor: pointer;
color: var(--te-common-text-primary);
}
}
</style>

View File

@ -1,5 +0,0 @@
import MyInputConfigurator from './MyInputConfigurator.vue'
export const configurators = {
MyInputConfigurator
}

View File

@ -1,27 +0,0 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { configurators } from './configurators/'
import 'virtual:svg-icons-register'
async function startApp() {
const registry = await import('../registry')
const { init } = await import('@opentiny/tiny-engine')
init({
// 合并多个注册表
registry: [registry.default],
configurators,
createAppSignal: ['global_service_init_finish']
})
}
startApp()

View File

@ -1,41 +0,0 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { defineEntry } from '@opentiny/tiny-engine-meta-register'
import engineConfig from '../engine.config'
import 'virtual:svg-icons-register'
async function startApp() {
const { initHook, HOOK_NAME, META_SERVICE, initPreview } = await import('@opentiny/tiny-engine')
const { HttpService } = await import('./composable')
const beforeAppCreate = () => {
initHook(HOOK_NAME.useEnv, import.meta.env)
}
const registry = {
[META_SERVICE.Http]: HttpService,
'engine.config': {
...engineConfig
}
}
defineEntry(registry)
initPreview({
registry,
lifeCycles: {
beforeAppCreate
}
})
}
startApp()

View File

@ -1,74 +0,0 @@
/**
* localCDN 功能测试
* 这个测试文件用于验证 localCDN 本地化功能是否正常工作
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { execSync } from 'node:child_process'
import { ensureEnvVarEnabled, backupEnvFile, restoreEnvFile } from './utils/envHelpers.js'
// 获取当前文件目录
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const projectRoot = path.resolve(__dirname, '..')
const distDir = path.resolve(projectRoot, 'dist')
const localCdnDir = path.resolve(distDir, 'local-cdn-static')
const envAlphaPath = path.resolve(projectRoot, 'env', '.env.alpha')
describe('localCDN 功能测试', () => {
beforeAll(() => {
// 备份环境变量文件
backupEnvFile(envAlphaPath)
// 确保环境变量正确设置
let envContent = fs.readFileSync(envAlphaPath, 'utf-8')
// 确保关键环境变量已启用
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_IMPORT_MAPS')
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_BUNDLE_DEPS')
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_IMPORT_PATH', 'local-cdn-static')
// 写回更新后的环境变量
fs.writeFileSync(envAlphaPath, envContent)
// 执行构建
execSync('pnpm run build:alpha', {
cwd: projectRoot,
stdio: 'inherit'
})
})
// 测试结束后恢复原始环境变量
afterAll(() => {
restoreEnvFile(envAlphaPath)
})
it('应该在构建后生成 local-cdn-static 目录', () => {
expect(fs.existsSync(localCdnDir)).toBe(true)
})
it('应该正确复制 @vue/devtools-api 依赖', () => {
// 寻找 @vue/devtools-api 文件夹
const devToolsDirs = fs.readdirSync(path.resolve(localCdnDir, '@vue'))
.find(dir => dir.startsWith('devtools-api@'))
expect(devToolsDirs).toBeDefined()
// 检查 index.js 是否存在
const indexJsExists = fs.existsSync(path.resolve(localCdnDir, '@vue', devToolsDirs, 'lib/esm/index.js'))
expect(indexJsExists).toBe(true)
})
it('应该正确复制 vue 依赖', () => {
// 寻找 vue 文件夹
const runtimeDirs = fs.readdirSync(localCdnDir).find(dir => dir.startsWith('vue@'))
expect(runtimeDirs).toBeDefined()
const vueProdDist = path.resolve(localCdnDir, runtimeDirs, 'dist/vue.runtime.esm-browser.js')
expect(fs.existsSync(vueProdDist)).toBe(true)
})
})

View File

@ -1,140 +0,0 @@
/**
* localCDN bundle依赖本地化测试
* 测试物料需要的CDN资源本地化功能
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { execSync } from 'node:child_process'
import { ensureEnvVarEnabled, updateCdnDomain, backupEnvFile, restoreEnvFile } from './utils/envHelpers.js'
// 获取当前文件目录
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const projectRoot = path.resolve(__dirname, '..')
const publicDir = path.resolve(projectRoot, 'public')
const bundleJsonDir = path.resolve(publicDir, 'mock')
const envAlphaPath = path.resolve(projectRoot, 'env', '.env.alpha')
const distDir = path.resolve(projectRoot, 'dist')
const bundleJsonPath = path.resolve(bundleJsonDir, 'bundle.json')
// 准备测试用的 bundle.json 文件
const testBundleJson = {
data: {
materials: {
packages: [
{
"name": "TinyVue组件库",
"package": "@opentiny/vue",
"version": "3.20.0",
"script": "https://unpkg.com/@opentiny/vue-runtime@3.20/dist3/tiny-vue-pc.mjs",
"css": "https://unpkg.com/@opentiny/vue-theme@3.20/index.css"
},
{
"name": "element-plus组件库",
"package": "element-plus",
"version": "2.4.2",
"script": "https://registry.npmmirror.com/element-plus/2.4.2/files/dist/index.full.mjs",
"css": "https://registry.npmmirror.com/element-plus/2.4.2/files/dist/index.css"
}
]
}
}
}
describe('localCDN bundle依赖本地化测试', () => {
let originalBundleJson = null
beforeAll(() => {
// 备份环境变量
backupEnvFile(envAlphaPath)
// 确保目录存在
if (!fs.existsSync(bundleJsonDir)) {
fs.mkdirSync(bundleJsonDir, { recursive: true })
}
// 备份原始的 bundle.json 文件(如果存在)
if (fs.existsSync(bundleJsonPath)) {
originalBundleJson = fs.readFileSync(bundleJsonPath, 'utf-8')
}
// 创建测试用的 bundle.json
fs.writeFileSync(bundleJsonPath, JSON.stringify(testBundleJson, null, 2))
// 设置环境变量
let envContent = fs.readFileSync(envAlphaPath, 'utf-8')
// 更新CDN域名
envContent = updateCdnDomain(envContent, 'https://unpkg.com')
// 确保启用了 bundle 依赖本地化
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_BUNDLE_DEPS')
fs.writeFileSync(envAlphaPath, envContent)
// 执行构建
execSync('pnpm run build:alpha', {
cwd: projectRoot,
stdio: 'inherit'
})
})
// 测试完成后清理测试文件并恢复环境变量
afterAll(() => {
// 恢复原始的 bundle.json 文件
if (originalBundleJson) {
fs.writeFileSync(bundleJsonPath, originalBundleJson)
} else if (fs.existsSync(bundleJsonPath)) {
// 如果原始文件不存在,则删除测试创建的文件
fs.unlinkSync(bundleJsonPath)
}
// 恢复环境变量
restoreEnvFile(envAlphaPath)
})
it('应该将物料CDN依赖本地化', () => {
const distBundleJsonPath = path.resolve(distDir, 'mock', 'bundle.json')
// 检查构建后的 bundle.json 是否存在
expect(fs.existsSync(distBundleJsonPath)).toBe(true)
// 检查构建后的 bundle.json 中是否已将远程URL替换为本地路径
const packages = JSON.parse(fs.readFileSync(distBundleJsonPath, 'utf-8')).data.materials.packages
// 检查 vue 的路径是否已本地化
expect(packages[0].script).not.toContain('https://unpkg.com')
expect(packages[0].script).toContain('./material-static/')
// 检查 element-plus 的路径是否已本地化
expect(packages[1].script).toContain('https://registry.npmmirror.com')
expect(packages[1].script).not.toContain('./material-static/')
expect(packages[1].css).toContain('https://registry.npmmirror.com')
expect(packages[1].css).not.toContain('./material-static/')
})
it('应该将物料依赖包复制到产物CDN目录', () => {
const localCdnDir = path.resolve(distDir, 'material-static/@opentiny')
// 检查 vue 是否已复制
const tinyVueDir = fs.readdirSync(localCdnDir)
.find(dir => dir.startsWith('vue-runtime@'))
const tinyVueThemeDir = fs.readdirSync(localCdnDir)
.find(dir => dir.startsWith('vue-theme@'))
expect(tinyVueDir).toBeDefined()
// 检查 tiny-vue-pc.mjs 是否存在
const tinyVueJsPath = path.resolve(localCdnDir, tinyVueDir, 'dist3', 'tiny-vue-pc.mjs')
const tinyVueCssPath = path.resolve(localCdnDir, tinyVueThemeDir, 'index.css')
expect(fs.existsSync(tinyVueJsPath)).toBe(true)
expect(fs.existsSync(tinyVueCssPath)).toBe(true)
// 检查 element-plus 是否已复制
const elementDir = fs.readdirSync(localCdnDir)
.find(dir => dir.startsWith('element-plus@'))
expect(elementDir).not.toBeDefined()
})
})

View File

@ -1,159 +0,0 @@
/**
* localCDN 自定义配置测试
* 测试文档中描述的自定义配置功能
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { execSync } from 'node:child_process'
import { ensureEnvVarEnabled, backupEnvFile, restoreEnvFile } from './utils/envHelpers.js'
// 获取当前文件目录
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const projectRoot = path.resolve(__dirname, '..')
const viteConfigPath = path.resolve(projectRoot, 'vite.config.js')
const envAlphaPath = path.resolve(projectRoot, 'env', '.env.alpha')
const originalViteConfig = fs.readFileSync(viteConfigPath, 'utf-8')
const distDir = path.resolve(projectRoot, 'dist')
/**
* 更新 registry.js 文件以支持自定义 importMap
*/
function updateRegistryFile() {
const registryPath = path.resolve(projectRoot, 'registry.js')
if (!fs.existsSync(registryPath)) {
return
}
// 备份原始文件
fs.copyFileSync(registryPath, registryPath + '.bak')
const registryContent = fs.readFileSync(registryPath, 'utf-8')
// 向 config 对象添加 importMap
const updatedContent = registryContent.replace(
/config: {([^}]*)}/,
`config: {$1,
importMap: {
imports: {
'vue': "\${VITE_CDN_DOMAIN}/vue\${versionDelimiter}3.4.21\${fileDelimiter}/dist/vue.runtime.esm-browser.js"
}
}
}`
)
fs.writeFileSync(registryPath, updatedContent)
}
/**
* 恢复 registry.js 文件
*/
function restoreRegistryFile() {
const registryPath = path.resolve(projectRoot, 'registry.js')
const backupPath = registryPath + '.bak'
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, registryPath)
fs.unlinkSync(backupPath)
}
}
describe('localCDN 自定义配置测试', () => {
beforeAll(() => {
// 备份原始的 vite.config.js
fs.writeFileSync(viteConfigPath + '.bak', originalViteConfig)
// 备份环境变量文件
backupEnvFile(envAlphaPath)
// 修改 vite.config.js 添加自定义配置
const updatedViteConfig = originalViteConfig.replace(
'const baseConfig = useTinyEngineBaseConfig({',
`const baseConfig = useTinyEngineBaseConfig({
importMapLocalConfig: {
importMap: {
imports: {
'vue': "\${VITE_CDN_DOMAIN}/vue\${versionDelimiter}3.4.21\${fileDelimiter}/dist/vue.runtime.esm-browser.prod.js"
}
},
copy: {
'vue': {
filePathInPackage: '/dist/'
}
}
},`
)
fs.writeFileSync(viteConfigPath, updatedViteConfig)
// 确保环境变量设置
let envContent = fs.readFileSync(envAlphaPath, 'utf-8')
// 确保关键环境变量已启用
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_IMPORT_MAPS')
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_BUNDLE_DEPS')
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_IMPORT_PATH', 'local-cdn-static')
// 写回更新后的环境变量
fs.writeFileSync(envAlphaPath, envContent)
// 修改 registry.js 以支持自定义 importMap
updateRegistryFile()
// 执行构建
execSync('pnpm run build:alpha', {
cwd: projectRoot,
stdio: 'inherit'
})
})
// 测试完成后恢复原始配置
afterAll(() => {
// 恢复 vite.config.js
if (fs.existsSync(viteConfigPath + '.bak')) {
fs.copyFileSync(viteConfigPath + '.bak', viteConfigPath)
fs.unlinkSync(viteConfigPath + '.bak')
}
// 恢复环境变量
restoreEnvFile(envAlphaPath)
// 恢复 registry.js
restoreRegistryFile()
})
it('应该正确应用自定义 importMap 配置', () => {
const localCdnDir = path.resolve(distDir, 'local-cdn-static')
// 检查 vue 是否被正确复制
const vueDirs = fs.readdirSync(localCdnDir)
.filter(dir => dir.startsWith('vue@'))
.map(dir => path.resolve(localCdnDir, dir))
expect(vueDirs.length).toBeGreaterThan(0)
// 检查 dist 目录是否存在
const distExists = vueDirs.some(dir => {
return fs.existsSync(path.resolve(dir, 'dist'))
})
expect(distExists).toBe(true)
// 检查 vue.global.prod.js 文件是否存在
const vueJsExists = vueDirs.some(dir => {
return fs.existsSync(path.resolve(dir, 'dist', 'vue.runtime.esm-browser.js'))
})
expect(vueJsExists).toBe(true)
// 检查 dist 目录下的文件数量是否大于1 (因为我们的复制配置是复制整个文件夹)
const distFileCount = vueDirs.some(dir => {
const distPath = path.resolve(dir, 'dist')
return fs.existsSync(distPath) && fs.readdirSync(distPath).length > 1
})
expect(distFileCount).toBe(true)
})
})

View File

@ -1,69 +0,0 @@
/**
* 环境变量处理工具函数
*/
import fs from 'node:fs'
/**
* 更新环境变量如果变量不存在或值为false则设置为true
* @param {string} content - 环境变量文件内容
* @param {string} key - 环境变量名
* @returns {string} - 更新后的内容
*/
export function ensureEnvVarEnabled(content, key, value = 'true') {
// 检查是否包含该环境变量
const regex = new RegExp(`${key}\\s*=\\s*(.*)`, 'm')
const match = content.match(regex)
if (!match) {
// 变量不存在,添加
return `${content}\n${key}=${value}`
} else if (match[1].trim() !== value) {
// 变量存在但值为跟 value 不相等,替换为提供的值 value
return content.replace(regex, `${key}=${value}`)
}
// 变量已存在且不是false保持不变
return content
}
/**
* 更新环境变量中的CDN域名
* @param {string} content - 环境变量文件内容
* @param {string} cdnDomain - CDN域名
* @returns {string} - 更新后的内容
*/
export function updateCdnDomain(content, cdnDomain) {
const regex = /VITE_CDN_DOMAIN\s*=\s*(.*)/m
const match = content.match(regex)
if (!match) {
return `${content}\nVITE_CDN_DOMAIN=${cdnDomain}`
} else {
return content.replace(regex, `VITE_CDN_DOMAIN=${cdnDomain}`)
}
}
/**
* 备份环境变量文件
* @param {string} envFilePath - 环境变量文件路径
* @returns {void}
*/
export function backupEnvFile(envFilePath) {
if (fs.existsSync(envFilePath)) {
const backupPath = envFilePath + '.bak'
fs.copyFileSync(envFilePath, backupPath)
}
}
/**
* 恢复环境变量文件
* @param {string} envFilePath - 环境变量文件路径
* @returns {void}
*/
export function restoreEnvFile(envFilePath) {
const backupPath = envFilePath + '.bak'
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, envFilePath)
fs.unlinkSync(backupPath)
}
}

View File

@ -1,24 +0,0 @@
import path from 'node:path'
import { defineConfig, mergeConfig } from 'vite'
import { useTinyEngineBaseConfig } from '@opentiny/tiny-engine-vite-config'
export default defineConfig((configEnv) => {
const baseConfig = useTinyEngineBaseConfig({
viteConfigEnv: configEnv,
root: __dirname,
iconDirs: [path.resolve(__dirname, './node_modules/@opentiny/tiny-engine/assets/')],
useSourceAlias: true,
envDir: './env',
registryPath: './registry.js'
})
const customConfig = {
envDir: './env',
publicDir: path.resolve(__dirname, './public'),
server: {
port: 8090
}
}
return mergeConfig(baseConfig, customConfig)
})

View File

@ -1,19 +0,0 @@
import { defineConfig } from 'vitest/config'
import path from 'node:path'
export default defineConfig({
test: {
globals: true,
environment: 'node',
testTimeout: 1_000 * 60 * 10, // 10分钟超时因为构建可能需要较长时间
include: ['tests/**/*.test.js'],
hookTimeout: 1_000 * 60 * 10, // 10分钟超时因为构建可能需要较长时间
// 这里需要串行执行,否则构建会相互覆盖,无法测试
fileParallelism: false
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})

View File

@ -1,125 +0,0 @@
# 目录
## 使用指南
- 新手指引
- [简介](./getting-started/introduction.md)
- [快速上手](./getting-started/quick-start.md)
- 基础功能
- [初识设计器](./basic-features/intro-to-designer.md)
- [设计前端应用流程](./basic-features/frontend-application-flow.md)
- [设计器界面模块简介](./basic-features/designer-ui-modules.md)
- [页面管理](./basic-features/page-management.md)
- [使用组件](./basic-features/using-components.md)
- [样式设置](./basic-features/style-settings.md)
- [使用状态管理和变量绑定](./basic-features/state-management-and-variable-binding.md)
- [行内样式绑定状态变量](./basic-features/inline-style-variable-binding.md)
- [查看大纲树](./basic-features/outline-tree.md)
- [国际化](./basic-features/internationalization.md)
- [页面和区块预览](./basic-features/page-and-block-preview.md)
- 进阶功能
- [区块管理](./advanced-features/block-management.md)
- [使用JS面板和事件绑定](./advanced-features/js-panel-and-event-binding.md)
- [使用工具类方法 utils](./advanced-features/using-utils-methods.md)
- [高级面板设置](./advanced-features/advanced-panel-settings.md)
- [如何使用插槽](./advanced-features/how-to-use-slots.md)
- [循环渲染](./advanced-features/loop-rendering.md)
- [条件渲染](./advanced-features/conditional-rendering.md)
- [集成ChatGPT搭建简单页面能力](./advanced-features/integrating-chatgpt-for-simple-pages.md)
- [数据源和Collection—远程字段](./advanced-features/data-source-and-collection-remote-fields.md)
- [数据源和Collection—mock数据](./advanced-features/data-source-and-collection-mock-data.md)
- [数据源和Collection—使用数据源](./advanced-features/data-source-and-collection-usage.md)
- 路由能力
- [页面支持嵌套路由](./advanced-features/route-capabilities/page-support-nested-route.md)
- [路由bar一键清除预览页面路径](./advanced-features/route-capabilities/route-bar-clear-preview-page.md)
- [路由bar高亮显示预览页面路径](./advanced-features/route-capabilities/route-bar-current-page-highlight.md)
- [RouterView组件支持预览子界面](./advanced-features/route-capabilities/route-view-support-preview-subpage.md)
- [主题切换功能](./advanced-features/theme-switch.md)
- [画布快捷操作](./advanced-features/canvas-shortcuts.md)
- 教程
- [从零搭建一个页面](./tutorials/build-a-page-from-scratch.md)
- [第一期2023.10.27](./tutorials/issue-1-2023.10.27.md)
## 平台开发指南
- 开始
- [简介](./development-getting-started/dev-intro.md)
- [快速上手](./development-getting-started/dev-quick-start.md)
- [前后端启动联调(Java服务端)](./development-getting-started/debugging-of-java-backend.md)
- 更新日志
- [更新日志](./changelog/changelog.md)
- [v2.7升级指南](./changelog/v2.7-upgrade-guide.md)
- 解决方案
- [前端及Java服务端docker部署](./solutions/front-backend-docker-deployment.md)
- [Java服务端部署](./solutions/server-deployment-solution-java.md)
- [Node.js服务端部署](./solutions/server-deployment-solution.md)
- [区块发布方案(Node.js服务端)](./solutions/block-release-solution.md)
- [区块局域网发布方案(Node.js服务端)](./solutions/block-lan-release-solution.md)
- [设计器中引入第三方组件库](./solutions/third-party-library-in-designer.md)
- [物料同步方案](./solutions/material-sync-solution.md)
- [本地化CDN方案](./solutions/import-map-local.md)
- [全新区块构建方案](./solutions/block-construction-solution.md)
- [全新画布通信方案](./solutions/canvas-communication-solution.md)
- 扩展能力介绍
- [新架构介绍](./extension-capabilities-overview/new-architecture.md)
- [注册表](./extension-capabilities-overview/registry.md)
- [注册表(新版)](./extension-capabilities-overview/new-registry.md)
- [注册表高级特性](./extension-capabilities-overview/new-registry-advanced.md)
- [元服务和元应用](./extension-capabilities-overview/meta-services-and-meta-apps.md)
- 扩展能力使用教程
- [如何开发插件](./extension-capabilities-tutorial/how-to-develop-plugins.md)
- 出码功能
- [出码功能简介与使用](./extension-capabilities-tutorial/code-output-function/code-output-overview-and-usage.md)
- [如何自定义出码](./extension-capabilities-tutorial/code-output-function/how-to-customize-code-output.md)
- [如何自定义出码插件](./extension-capabilities-tutorial/code-output-function/how-to-customize-code-output-plugins.md)
- [自定义页面出码插件](./extension-capabilities-tutorial/code-output-function/custom-page-code-output-plugin.md)
- [官方出码能力API](./extension-capabilities-tutorial/code-output-function/official-code-output-api.md)
- [定制插件UI](./extension-capabilities-tutorial/customize-plugin-ui.md)
- [定制元服务逻辑](./extension-capabilities-tutorial/customize-meta-service-logic.md)
- [开发设置器组件](./extension-capabilities-tutorial/develop-configurator-components.md)
- [AI插件使用前配置](./extension-capabilities-tutorial/ai-plugin-configuration.md)
- [如何自定义主题](./extension-capabilities-tutorial/how-to-custom-theme.md)
- API
- [API总览](./api/api-overview.md)
- 前端API
- [主包API](./api/frontend-api/main-package-api.md)
- [画布API](./api/frontend-api/canvas-api.md)
- [全局布局API](./api/frontend-api/global-layout-api.md)
- [物料API](./api/frontend-api/material-api.md)
- [设置面板API](./api/frontend-api/settings-panel-api.md)
- [预览API](./api/frontend-api/preview-api.md)
- 后端API
- [AI功能接口](./api/backend-api/ai-function-api.md)
- [应用管理](./api/backend-api/app-management.md)
- [区块分类](./api/backend-api/block-categories.md)
- [应用工具类管理](./api/backend-api/app-utility-management.md)
- [区块管理](./api/backend-api/block-management-api.md)
- [数据源管理](./api/backend-api/data-source-management.md)
- [物料中心](./api/backend-api/material-center.md)
- [页面管理](./api/backend-api/page-management-api.md)
- [APP服务](./api/backend-api/app-services.md)
- 实战案例
- [PDM元数据审批电子流](./practical-cases/pdm-metadata-approval-workflow.md)
- [图元编排设计器](./practical-cases/graphical-element-arrangement-designer.md)
- [SMB轻量应用服务](./practical-cases/smb-lightweight-application-service.md)
## 网站文档
- 生态中心
- [介绍](./ecosystem-center/ecosystem-intro.md)
- [如何导入组件库](./ecosystem-center/how-to-import-library.md)
- [如何发布区块](./ecosystem-center/how-to-publish-block.md)
- [发布其他生态](./ecosystem-center/publish-other-ecosystems.md)
- 关于应用
- [创建应用(创建空白应用、从模板创建应用)](./about-applications/create-application-blank-or-template.md)
- [开发应用](./about-applications/develop-application.md)
- 关于物料
- [介绍](./about-materials/materials-intro.md)
- [创建物料资产包](./about-materials/create-material-asset-package.md)
- [添加组件库和区块](./about-materials/add-library-and-blocks.md)
- [构建物料资产包](./about-materials/build-material-asset-package.md)
- 关于设计器
- [介绍](./about-designer/designer-intro.md)
- [创建设计器](./about-designer/create-designer.md)
- [定制物料资产包、主题、DSL、工具栏和插件栏](./about-designer/customize-material-package-themes-dsl-toolbar-plugins.md)
- [定制设计器](./about-designer/customize-designer.md)

View File

@ -1,13 +0,0 @@
# 关于应用
## 如何创建应用
设计器定制完成后,用户可以在 [我的设计器](https://www.opentiny.design/tiny-engine#/my-platform) 中创建应用。选择对应的设计器,在设计器右下方点击“创建应用”。
创建应用有两种方式:
1.创建空白应用 &rarr; 填写必要的字段 &rarr; 保存。
![创建应用方式一]( ./imgs/appCreateOne.png)
2.从模板创建应用&rarr; 选择应用模板 &rarr; 填写必要的字段 &rarr; 保存。
![创建应用方式二]( ./imgs/appCreateTwo.png)

View File

@ -1,11 +0,0 @@
# 关于应用
## 如何去开发应用
应用创建完成后,即可在 **我的应用** 中看到这个应用,点击 **开发应用** 即可前往可视化设计器进行可视化地搭建该应用下的 *页面**区块*
![view-app1]( ./imgs/appDevOne.png)
### 可视化设计器
![view-app2]( ./imgs/appDevTwo.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

View File

@ -1,6 +0,0 @@
# 关于设计器
## 如何创建一个设计器
用户可以在 [我的设计器](https://www.opentiny.design/tiny-engine#/my-platform) 中创建设计器,创建设计器 &rarr; 填写必要的字段 &rarr; 确定
![create-platform]( ./imgs/createPlatform.png)

View File

@ -1,13 +0,0 @@
# 关于设计器
## 如何定制一个设计器
物料资产包、主题、工具、插件和DSL定制完成后有两种方式可以构建设计器。
方式一:可视化构建
![build-platform1]( ./imgs/buildPlatform1.png)
方式二:由源码构建
![build-platform2]( ./imgs/buildPlatform2.png)

View File

@ -1,20 +0,0 @@
# 关于设计器
## 如何定制物料资产包、主题、DSL、工具栏和插件栏
设计器创建完成后会自动打开编辑页面,用户可以在这里定制设计器的*物料资产包*、*主题*、*工具*、*插件*和*DSL*,如下图:
1. **定制物料资产包:** 物料资产包 **必选且唯一** , **不允许删除** 物料资产包,可以添加其他物料资产包来替换当前的。
![define-material]( ./imgs/defineMaterial.png)
2. **定制主题:** 主题 **必选且唯一** **不允许删除** 主题,可以添加其他主题来替换当前的
![define-theme]( ./imgs/defineTheme.png)
3. **定制DSL** DSL为 **单选**。DSL是将物料的Schema 解析成不同技术栈源码的转换工具所以DSL的必须和选择的物料在技术栈保持一致。
![define-DSL]( ./imgs/defineDsl.png)
4. **定制工具:** 可以将工具拖入上方位置栏,规划定制的设计器里工具的位置。工具可以多选,也可以删除。
![define-tools]( ./imgs/defineTools.png)
5. **定制插件:** 可以将插件拖入中间位置栏,规划定制的设计器里插件的位置。插件可以多选,也可以删除。
![define-plugs]( ./imgs/definePlugs.png)

View File

@ -1,6 +0,0 @@
# 构建自定义设计器
## 什么是定制扩展能力
什么是扩展能力呢,一方面我们可以快速拥有一个官方标准的设计器,另外一方面如果用户有独特的业务功能需要,我们可以不用看它的源码、不用关心其实现,用户可以使用 API、插件等方式快速开发自己的工具插件DSL等的npm包用于构建用户自定义的设计器。而设计器引擎对于设计器的扩展能力支持基本上覆盖了设计器的所有功能点。
![expend]( ./imgs/platformExpend.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

View File

@ -1,19 +0,0 @@
# 添加组件库/区块
## 如何添加组件库与区块
可视化设计器已为您提供官方组件库与一些区块,物料资产包创建完成后会自动打开编辑页面,用户可以在这里添加组件库与区块,如下图:
* **添加组件库:** 选中未选择的组件库
![添加组件库](./imgs/createMaterialLib.png)
* **添加区块:** 选中未选择的区块
![添加区块](./imgs/createMaterialBlock.png)
* **移除组件库:** 取消选中已经选择的组件库,并且确认
![移除组件库](./imgs/createMaterialLibCancel.png)
* **移除区块:** 取消选中已经选择的区块,并且确认
![移除区块](./imgs/createMaterialBlockCancel.png)
* 用户也可以在生态中心录入自己的组件库与区块。

View File

@ -1,6 +0,0 @@
# 构建物料资产包
## 如何构建物料资产包
添加完组件库与区块后,点击底部 **构建物料资产包** 按钮,即可完成物料资产包的构建。构建完成后即可看到发布地址。
![构建物料资产包](./imgs/createMaterial.gif)

View File

@ -1,10 +0,0 @@
# 创建物料资产包
## 如何创建物料资产包
用户可以在生态中心创建物料资产包,*新建物料资产包* → *填写必要的字段* → *保存。*
当前支持Vue和Angular两种技术栈用户可以任意选择。当用户选择了Vue技术栈时则只能添加Vue的组件与区块不能添加Angular的组件库与区块。
物料资产包版本是用户自定义的,用户可以将物料资产包回退到任一版本。
![构建物料资产包](./imgs/createMaterial.gif)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,16 +0,0 @@
# 关于物料资产包
## 什么是物料
物料是可视化页面搭建的原料,按照粒度可分为组件库和区块
1. **组件库**:组件库一般是按照组件的性质进行组织。组件是页面搭建最小的可复用单元,其只对外暴露配置项,用户无需感知其内部实现。组件库在设计器定制完成后 **不能再次添加与删除**
2. **区块**:区块可以包含一个或多个组件也可以包含其他区块,带有一定的业务逻辑,能够实现更丰富的功能与表现。区块分为**消费侧**和**管理侧**,在区块管理侧,用户可以向设计器中拖入一个区块后可以编辑其内部的组件和区块配置,暴露区块的属性和事件供消费区块的时候使用,最后发布区块,消费侧区块就会更新为最新的区块内容。区块在设计器定制完成后仍能添加与删除。
设计器中的物料需要进行一定的配置和处理,才能让用户在设计器使用起来。这个过程中,需要一份配置文件,也就是物料资产包。物料资产包文件中,针对每个物料定义了它们在设计器中的使用描述。
## 什么是物料资产包
在设计器中,我们可以看到,组件与区块面板不只提供一个组件或区块,它们是以集合的形式提供给设计器的,而物料资产包正是这些组件与区块构成的集合。
![view-material](./imgs/materialPanel.png)

View File

@ -1,69 +0,0 @@
# 高级面板设置
> 选中组件之后,我们可以在高级面板对组件进行事件的绑定、以及循环渲染、条件渲染等高级设置
## 条件渲染
在页面开发中,我们可能需要根据某些条件来动态显示或隐藏页面中的内容,举个例子:我们希望当用户已经登录的时候,显示欢迎登录的文字,未登录的时候,显示请登录的文字。
那么,我们可以在组件上面绑定条件渲染,点击绑定变量,选择变量 state.isLogin那么我们的组件就会根据变量 state.isLogin 的真假值来进行渲染。
![条件渲染示例](./imgs/conditionRender.gif)
## 循环渲染
我们的页面可能有若干份重复的、动态生成的内容,比如商品列表页,比如表格数据。这时候,我们就需要用到循环渲染
我们可以在高级面板中指定循环数据绑定的变量、迭代的变量名、索引变量名、以及唯一的 key。
举例:假如我们的状态变量中有一个镜像列表,我们希望渲染出来镜像的 icon、镜像名称以及镜像版本那么我们可以使用循环渲染来实现
![循环渲染](./imgs/useLoop.gif)
相关概念关联:
- 循环数据,即需要循环渲染的数组,在这里是 state.imageList
- 迭代变量名,在循环渲染子项对应的变量名,默认为 item
- 索引变量名,循环渲染的索引变量名,默认为 index
- key标识唯一的 key默认为 index
最终出码:
```vue
<template>
<div v-for="(item, index) in state.imageList" :key="index">
<span>{{ item.title }}</span>
<!---列表细节--->
</div>
</template>
```
## 事件绑定
我们可以给选中的组件进行事件的绑定:
- 在绑定事件下拉按钮中选中需要绑定的事件
- 随后在事件绑定弹窗中指定方法名称
- 指定拓展参数
- 点击确定在随后弹出的JS面板中进行绑定方法的具体逻辑实现
![事件绑定演示](./imgs/bindEvent.gif)
### 拓展参数相关说明
在某些点击事件中,我们不仅仅希望得知事件是否被点击,我们还希望在点击事件中获得一些额外的参数,这时候,我们就可以用拓展参数,下面举例说明:
我们希望为镜像列表中的列表项绑定点击事件,然后将镜像的 id 和版本传入到事件处理函数中。那么,我们就可以使用拓展参数进行传入。
```bash
# 循环渲染项迭代变量名 item
# 拓展参数设置:
["item.imageId", "item.imageVersion"]
```
最终出码:
```vue
<template>
<div v-for="(item, index) in state.imageList" @click="(e) => handleClick(e, item.imageId, item.imageVersion)">
<!---列表细节--->
</div>
</template>
```

View File

@ -1,129 +0,0 @@
# 区块管理
> 区块的概念类似于前端开发中的 Component我们可以将很多页面中都一样的结构比如 Header构建到区块中发布之后直接在页面中拖入使用提高开发效率
## 区块相关概念
假设我们有如下前端工程:
```bash
- project
- components
|_ Header.vue
- Footer.vue
- Container.vue
- views
|_ Index.vue
- Page2.vue
- TodoFolder
|_ Todo.vue
```
其中views 文件夹下 views每个页面都有路由一一对应用户可以根据路由访问。components文件夹下的 Header、Footer、Container则没有对应路由可以访问但是他们可以被页面1、2、3引用提高代码复用率我们的区块则对应 components 下的Header、Footer、Container等组件概念即可重用的业务组件
### 区块发布相关概念
我们对区块编辑好之后,最终还是提供给另一个区块或者页面使用(即被另一个区块或者页面引用),所以我们设计了发布的概念,区块发布之后,会生成一个版本,可以在物料面板添加已经发布的区块,选择版本,然后拖入画布中消费使用
## 区块的基本使用与管理
创建区块可以有创建空白区块和从现有页面中创建区块两种
### 创建空白区块
![创建空白区块](./imgs/createEmptyBlock.png)
如上图,按照步骤可创建空白区块
- 点击左侧区块插件打开区块插件面板
- 点击右上角新增按钮
- 在弹窗中输入区块ID与区块名称
相关概念
- 区块ID区块的唯一ID对应出码后的区块文件名
- 区块名称:在区块管理面板和物料消费面板显示的名称
### 从现有页面中选中组件创建区块
![从现有页面中选中组件创建区块](./imgs/createBlockFromPage.gif)
如上图,我们可以从现有页面中,按照步骤创建新区块
- 点击画布选中组件
- 右键,在弹出的右键菜单中点击新建区块
- 在弹出的弹窗中输入新区块的ID和名称点击确认
- 最终我们得到了有选中组件作为初始内容的区块
## 区块管理与设置
在上述创建空白区块的区块管理插件中,我们还能看到现有的区块,对现有的区块进行修改和删除等管理操作,下面,我们来学习区块的几个管理属性
### 区块基本设置
![区块基本设置](./imgs/blockBaseSetting.png)
如上图,点击区块列表中区块的右上角的设置按钮,即可打开设置面板。区块的基本设置中,我们可以对区块的名称、描述、标签、公开范围进行设置,下面对相关设置项进行讲解
- 区块描述:区块的描述,可以让别人更好的明白该区块的用途以及含义
- 区块标签:区块的标签,方便消费侧用户搜索
- 公开范围:区块的公开范围,设置 区块发布之后别的用户是否可以搜索,私有即只有自己可以看到、公开即所有用户都可以看到、半公开可以选择可以搜索到该区块的组织
### 区块暴露属性设置
#### 区块暴露属性的相关概念
假设我们的前端工程中有一个 Header 组件,该组件定义了 title 和 description 两个 props 属性
```vue
<template>
<header>
<h1>{{props.title}}</h1>
<span>{{props.description}}</span>
</header>
</template>
<script setup>
const props = defineProps({
title: { type: String, default: '' },
description: { type: String, default: '' }
})
</script>
```
区块暴露属性的概念即对应 组件中的 props 属性。暴露属性声明了外部组件引用当前组件时,可以定义的相关属性
#### 区块暴露属性的相关设置
区块暴露属性的设置与我们代码 props 的设置相似,只是区块暴露属性多了一个指定属性面板组件和属性的相关配置
![区块暴露属性设置](./imgs/blockExposeAttr.gif)
#### 区块暴露属性的消费
在区块发布之后,我们在物料面板拖出区块到画布中,选中我们拖出的区块,右侧属性面板的属性即是我们定义的暴露属性
![区块暴露属性的消费](./imgs/blockexposeattrusage.gif)
### 区块事件设置
#### 区块事件的概念
既然区块暴露属性等同于 组件 中的 props 属性,类似的,区块的事件即等同于 vue 中的 emit 事件声明
```vue
<script setup>
const emit = defineEmits(['update:modelValue', 'success'])
</script>
```
注意的是,这里仅仅是声明我们的区块会抛出什么事件,真正的事件需要引用方(即消费该区块的一方在高级面板进行定义)
### 生命周期设置
同页面生命周期,即可以设置对应技术栈的生命周期函数
### 版本列表
可以预览对应版本的区块

View File

@ -1,27 +0,0 @@
# 画布快捷操作
## 支持画布选中元素右键添加父容器
• 在画布中右键页面元素 -> 添加父级 -> 容器:
![canvas-shortcuts-1](./imgs/canvas-shortcuts-1.png)
• 选中指定 容器 点击 鼠标左键:
![canvas-shortcuts-2](./imgs/canvas-shortcuts-2.png)
• 点击 画布元素 或 点击 大纲树 即可查看新增的父容器
![canvas-shortcuts-3](./imgs/canvas-shortcuts-3.png)
## 支持节点多选
![canvas-shortcuts-4](./imgs/canvas-shortcuts-4.png)
长按 ctrl + 鼠标单击,可支持元素多选,多选节点后可以结合快捷键可以实现批量复制、粘贴、删除操作。(多选节点后右键菜单能力后续版本持续完善)
## 快捷键梳理
![canvas-shortcuts-5](./imgs/canvas-shortcuts-5.png)
目前系统支持上述快捷键对画布元素进行操作,其中 复制、粘贴 和 删除 支持多选节点后进行批量操作。

View File

@ -1,30 +0,0 @@
# 条件渲染
在页面开发中,可能需要根据某些条件来动态显示或隐藏页面中的内容,例如:如果您希望当用户已经登录的时候,显示“欢迎登录“的文字,未登录的时候,显示“请登录“的文字。
1. 拖拽组件至画布,分别输入希望展示的文字。
2. 添加变量例如state.isLogin
**图 1** 添加变量
![addvar-15](./imgs/addvar-15.png)
3. 选中组件,在组件属性设置面板选择“高级”。
4. 单击“是否渲染“后的 <img src="./imgs/icon-code.png" alt="变量绑定图标" class="image-inline">,进行变量绑定。
**图 2** 绑定变量
![bindVariable-16.png](./imgs/bindVariable-16.png)
5. 选项绑定的变量,单击“确定”。
绑定成功后可根据变量state.isLogin的值查看渲染效果。
**图 3** state.isLogin为false时
![state-isLogin为false时](./imgs/state-isLogin为false时.png)
**图 4** state.isLogin为true时
![state-isLogin为ture时.png](./imgs/state-isLogin为true时.png)

View File

@ -1,21 +0,0 @@
# 数据源Mock数据
目前数据源在编辑态时只能使用Mock数据所以我们可能需要给数据源添加Mock数据
## 操作步骤
- 打开数据源面板,选中数据源
- 点击新增数据,添加数据并保存
![新增数据](./imgs/dataSource5.png)
## 使用数据源Mock数据
- 拖动一个Collection组件到画布中在Collection组件属性面板上选择数据源
- 拖动格组件到Collection中表格组件将自动生成数据源中的字段
![使用数据源Mock数据](./imgs/dataSource6.png)
## 更新数据源到画布
添加完Mock数据后画布上绑定的数据源不会同步改变需要手动更新操作如下
选中画布中的数据源,打开属性面板,点击更新数据源
![更新数据源到画布](./imgs/dataSource7.png)

View File

@ -1,27 +0,0 @@
# 数据源获取远程字段
我们可以通过已有的远程Http接口快速地生成数据源的字段
## 操作步骤
- 打开数据源面板,新建数据源。点击获取远程数据
- 设置请求地址、请求方式、请求参数、请求结果回调
![数据源设置](./imgs/dataSource1.png)
- 请求成功后获取到接口字段信息,填写字段名后,保存后即可生成数据源字段信息
![请求结果](./imgs/dataSource2.png)
![字段配置](./imgs/dataSource3.png)
![保存数据源](./imgs/dataSource4.png)
## 请求完成回调函数示例
- 解析对象数组
```javascript
function dataHandler(data) {
return data.map(v => {
return {
name: v.aa.ss,
status: v.status
}
})
}
```

View File

@ -1,127 +0,0 @@
# 数据源
### 前言
设计器提供数据源来配合画布上的组件/区块渲染,数据源的配置既可以采取在设计器中静态配置的方式,也可以采取远程 API 请求 JSON 数据动态获取的方式。
> 目前**数据源**可便捷地应用于表格组件的表格列,也可灵活地应用于手动调用指定的远程 API。
### 创建数据源
创建数据源步骤:
1. 选择左边操作栏 - 数据源
2. 点击左上操作区 - 新建数据源
![Alt text](./imgs/data-source-2.png)
3. 配置数据源类型(可选远程数据源、静态数据源),配置数据源名称以及数据源字段
4. 保存数据源
![Alt text](./imgs/data-source-1.png)
### 表格组件中的应用
数据源主要载体为**Collection组件**,因此在使用数据源之前需要先在画布中拖放入**Collection组件**,然后在属性面板中选择需要绑定的数据源
![Alt text](./imgs/data-source-3.png)
然后需要在Collection组件中放入Grid表格组件根据提示引入配置数据就会自动解析出表格列数据
![Alt text](./imgs/data-source-4.png)
### 手动调用指定的远程 API
低代码引擎,将所有数据源都挂载到了 `dataSourceMap` 中,并为每个数据源都提供了 `load` 方法,用于手动调用场景,比如:点击保存按钮时,需要调用后端的保存接口,提交用户填写的数据,此时可以通过数据源来提交。
#### 使用说明
`this.dataSourceMap.xxx.load()`
- xxx 为在数据源面板设置的数据源名称
- 支持传入请求参数,可用于覆盖在数据源面板中配置的请求参数(默认请求参数)
- load 方法返回一个 Promise
#### 示例
以会议预订页面为例,为 `创建` 按钮绑定点击事件(`onClick`
![Alt text](./imgs/data-source-load-1.png)
绑定点击事件处理器为 `createMeeting`,补充其实现,主要为 `this.dataSourceMap.createMeeting.load(this.state.meeting)`
表示将用户填写的会议信息(`this.state.meeting`),调用数据源 `addMeeting`POST 请求),提交给后端。
![Alt text](./imgs/data-source-load-2.png)
其中,数据源 `addMeeting` 配置示例如下:
![Alt text](./imgs/data-source-load-3.png)
完整代码示例如下:
```js
async function createMeeting() {
try {
const res = await this.dataSourceMap.addMeeting.load(this.state.meeting)
console.log('成功创建以下会议:', res)
} catch {
this.utils.toast({
type: 'error',
title: '创建会议请求失败,请稍候重试或联系客服'
})
}
}
```
# 数据源获取远程字段
在使用数据源之前,我们需要向数据源中添加属性字段,可以通过“新建字段”按钮逐个添加。
但某些场景下,有更高效的添加方式,比如:基于已有的 HTTP 接口响应数据,快速地创建数据源的字段。
## 操作步骤
- 打开数据源面板,新建数据源。点击获取远程数据
- 设置请求地址、请求方式、请求参数、请求结果回调
![数据源设置](./imgs/dataSource1.png)
- 请求成功后获取到接口字段信息,填写字段名后,保存后即可生成数据源字段信息
![请求结果](./imgs/dataSource2.png)
![字段配置](./imgs/dataSource3.png)
![保存数据源](./imgs/dataSource4.png)
> 如果接口请求存在跨域、鉴权等情况无法通过“发送请求”自动填充响应数据时可以手动将响应数据比如JSON 格式数据),粘贴至下方的“请求结果”编辑器中。
## 请求完成回调函数示例
- 解析对象数组
```javascript
function dataHandler(data) {
return data.map(v => ({
name: v.nickName,
status: v.status
})
)
}
```
# 数据源Mock数据
目前数据源在编辑态时只能使用Mock数据所以我们可能需要给数据源添加Mock数据
## 操作步骤
- 打开数据源面板,选中数据源
- 点击新增数据,添加数据并保存
![新增数据](./imgs/dataSource5.png)
## 使用数据源Mock数据
- 拖动一个Collection组件到画布中在Collection组件属性面板上选择数据源
- 拖动格组件到Collection中表格组件将自动生成数据源中的字段
![使用数据源Mock数据](./imgs/dataSource6.png)
## 更新数据源到画布
添加完Mock数据后画布上绑定的数据源不会同步改变需要手动更新操作如下
选中画布中的数据源,打开属性面板,点击更新数据源
![更新数据源到画布](./imgs/dataSource7.png)

View File

@ -1,165 +0,0 @@
# 插槽的声明与使用
## 插槽的相关概念
> 插槽的概念与 vue.js 的插槽 [slot](https://cn.vuejs.org/guide/components/slots.html) 概念一致
我们以上述 Header组件为例讲解插槽需要了解的核心概念
- 插槽名字:插槽名字默认为 default
- 作用域插槽:用于向插槽传入子组件状态
```vue
// Header.vue
<template>
<header>
<h1>TinyEngine</h1>
<slot name="menu" :menu="state.menu" :type="'mobile'">
<menu>
<li v-for="item in state.menu" :key="item.id">{{ item.name }}</li>
</menu>
</slot>
</header>
</template>
```
```vue
<template>
<Header>
<template #menu="{ menu, type }">
<ul>
<li v-for="item in menu" :key="item.id">{{ item.name }}</li>
</ul>
</template>
</Header>
</template>
```
如以上代码为例子,当我们使用 `Header` 组件的时候,我们可以声明 menu 插槽,并且在 menu 插槽里面获取到子组件 menu 的状态。此时即为作用域插槽。
## 插槽的声明
在区块中,我们可以通过拖入插槽的方式声明插槽。然后,我们可以在右侧设置面板设置插槽的名字,向插槽传入 props。
如果需要传入 props我们可以通过编辑代码写入我们需要的表达式将 props 传入 插槽中。
![插槽声明](./imgs/defineslot.gif)
我们得到的带有插槽的 schema 大致为以下样式:
```json5
{
"componentName": "Slot",
"props": {
"name": "menu",
"params": [
{
"name": "title",
"value": {
"type": "JSExpression",
"value": "this.state.ggggg"
}
}
]
},
"id": "25632b32",
"children": [
{
"componentName": "Text",
"props": {
"text": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。"
},
"id": "63246b33"
}
]
}
```
区块出码大致为:
```vue
<template>
<div>
<slot name="menu" :title="state.ggggg">
<span>TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。</span>
</slot>
</div>
</template>
```
最后,我们可以经过保存 -> 发布 -> 组件面板导入区块的流程,将区块变成可消费的组件。
## 插槽的使用
上一步,我们通过低代码的区块,或者导入的已有的组件声明插槽,那么我们将可以将组件或者区块拖入画布中,在右侧面板相关的配置项开启插槽。
然后画布会开启对应的插槽,我们可以往插槽中拖入编排插槽组件。
在拖入的组件中,我们可以通过绑定变量的方式获取传入的作用域插槽:
![拖入使用区块插槽](./imgs/usingslot.gif)
最终得到的 schema 大致为:
```json5
{
// 组件或者区块名
"componentName": "BlockFileName",
"props": {},
"componentType": "Block",
"id": "363d84ba",
"children": [
{
// 子组件是 template
"componentName": "Template",
"props": {
// slot 作为 props说明是插槽
"slot": {
// 插槽名称
"name": "menu",
// 插槽参数
"params": [
"title"
]
}
},
// 插槽内容
"children": [
{
"componentName": "Text",
"props": {
// 这里 text 使用作用域插槽传入的变量,我们绑定变量 title 即可生效
"text": {
"type": "JSExpression",
"value": "title"
}
},
"id": "24212a32"
}
],
"id": "42753254"
}
]
}
```
组件的插槽,也类似于区块插槽,我们在组件物料定义插槽即可:
```json5
{
// 物料 schema 定义
"schema": {
// 定义插槽
"slots": {
// 命名插槽,为 menu
"menu": {
// 在右侧属性面板显示的名称
"label": {
"zh_CN": "menu"
},
// 插槽参数名
"params": ["title"]
}
}
}
}
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Some files were not shown because too many files have changed in this diff Show More