diff --git a/.agents/skills/tinyengine-dsl-generator/SKILL.md b/.agents/skills/tinyengine-dsl-generator/SKILL.md new file mode 100644 index 0000000000..ae994fd101 --- /dev/null +++ b/.agents/skills/tinyengine-dsl-generator/SKILL.md @@ -0,0 +1,168 @@ +--- +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 ` | +| Validate output | `bash scripts/validate_all.sh ` (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 +``` + +`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/.json` +- **Pages** → `mockServer/data/pages/.json` +- **Blocks** → `mockServer/data/blocks/.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` diff --git a/.agents/skills/tinyengine-dsl-generator/references/components.md b/.agents/skills/tinyengine-dsl-generator/references/components.md new file mode 100644 index 0000000000..cb5c3be99d --- /dev/null +++ b/.agents/skills/tinyengine-dsl-generator/references/components.md @@ -0,0 +1,474 @@ +# 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 // 表格数据 + 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 + 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; + rules: Record; // 校验规则 + 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; +} +``` + +### 布局组件 + +#### 容器 (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 + id: string | number + }> + showCheckbox: boolean + checkOnClickNode: boolean + defaultExpandAll: boolean + filterNodeMethod: JSFunction + onCheckChange: JSFunction + onNodeClick: JSFunction +} +``` + +#### 分页 (TinyPager) + +```typescript +interface TinyPagerProps { + currentPage: number + pageSizes: Array + 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 + 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 + settings: { + dimensions?: string[] + metrics?: string[] + xAxisType?: 'category' | 'value' | 'time' + yAxisType?: 'category' | 'value' | 'time' + yAxisName?: string[] + area?: boolean + } +} +``` + +### 柱状图 (TinyBarChart) + +```typescript +interface TinyBarChartProps { + data: Array + settings: { + dimensions?: string[] + metrics?: string[] + axisSite?: { top?: string[] } + label?: { show?: boolean } + } +} +``` + +### 饼图 (TinyPieChart) + +```typescript +interface TinyPieChartProps { + data: Array + 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 参考。 diff --git a/.agents/skills/tinyengine-dsl-generator/references/patterns.md b/.agents/skills/tinyengine-dsl-generator/references/patterns.md new file mode 100644 index 0000000000..1deb53a4bd --- /dev/null +++ b/.agents/skills/tinyengine-dsl-generator/references/patterns.md @@ -0,0 +1,1119 @@ +# TinyEngine DSL Common Patterns + +本文档包含生成 TinyEngine DSL 时的常见模式和模板。 + +## 目录 + +1. [常用页面模板](#常用页面模板) +2. [常见交互模式](#常见交互模式) +3. [布局模式](#布局模式) +4. [数据流模式](#数据流模式) +5. [区块设计模式](#区块设计模式) + +--- + +## 常用页面模板 + +### 列表页模板 + +```json +{ + "componentName": "Page", + "fileName": "ListPage", + "meta": { + "id": 1, + "title": "列表页", + "router": "list", + "creator": "admin", + "isHome": false, + "parentId": "0", + "rootElement": "div", + "group": "staticPages", + "description": "通用列表页", + "gmt_create": "2024-01-01 00:00:00", + "gmt_modified": "2024-01-01 00:00:00" + }, + "state": { + "tableData": [], + "loading": false, + "pagination": { + "currentPage": 1, + "pageSize": 10, + "total": 0 + }, + "searchForm": {} + }, + "methods": { + "fetchData": { + "type": "JSFunction", + "value": "function() { this.state.loading = true; /* fetch data */ }" + }, + "handleSearch": { + "type": "JSFunction", + "value": "function() { this.state.pagination.currentPage = 1; this.fetchData(); }" + }, + "handleReset": { + "type": "JSFunction", + "value": "function() { this.state.searchForm = {}; this.fetchData(); }" + } + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "page-header" + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "type": "primary", + "text": "新增", + "onClick": { + "type": "JSExpression", + "value": "this.handleAdd" + } + }, + "id": "btn-add" + } + ], + "id": "header-001" + }, + { + "componentName": "TinyForm", + "props": { + "model": { + "type": "JSExpression", + "value": "this.state.searchForm" + }, + "labelWidth": "80px" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "名称" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "modelValue": { + "type": "JSExpression", + "value": "this.state.searchForm.name", + "model": { + "prop": "name" + } + }, + "placeholder": "请输入名称" + }, + "id": "input-name" + } + ], + "id": "form-item-name" + } + ], + "id": "search-form" + }, + { + "componentName": "TinyGrid", + "props": { + "data": { + "type": "JSExpression", + "value": "this.state.tableData" + }, + "columns": [ + { + "field": "id", + "title": "ID", + "width": 80 + }, + { + "field": "name", + "title": "名称" + }, + { + "field": "actions", + "title": "操作", + "fixed": "right", + "width": 200 + } + ], + "border": true + }, + "id": "data-grid" + }, + { + "componentName": "TinyPager", + "props": { + "currentPage": { + "type": "JSExpression", + "value": "this.state.pagination.currentPage" + }, + "pageSize": { + "type": "JSExpression", + "value": "this.state.pagination.pageSize" + }, + "total": { + "type": "JSExpression", + "value": "this.state.pagination.total" + }, + "layout": "total, sizes, prev, pager, next, jumper", + "onCurrentChange": { + "type": "JSExpression", + "value": "this.handlePageChange" + } + }, + "id": "pagination" + } + ] +} +``` + +### 表单页模板 + +```json +{ + "componentName": "Page", + "fileName": "FormPage", + "meta": { + "id": 2, + "title": "表单页", + "router": "form", + "creator": "admin", + "isHome": false, + "parentId": "0", + "rootElement": "div", + "group": "staticPages", + "gmt_create": "2024-01-01 00:00:00", + "gmt_modified": "2024-01-01 00:00:00" + }, + "state": { + "formData": {}, + "rules": { + "name": [{ "required": true, "message": "请输入名称" }], + "email": [{ "type": "email", "message": "请输入正确的邮箱" }] + } + }, + "methods": { + "handleSubmit": { + "type": "JSFunction", + "value": "async function() { const valid = await this.$refs.formRef.validate(); if (valid) { /* submit */ } }" + }, + "handleCancel": { + "type": "JSFunction", + "value": "function() { this.$router.back(); }" + } + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "ref": "formRef", + "model": { + "type": "JSExpression", + "value": "this.state.formData" + }, + "rules": { + "type": "JSExpression", + "value": "this.state.rules" + }, + "labelWidth": "100px" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "名称", + "prop": "name", + "required": true + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "modelValue": { + "type": "JSExpression", + "value": "this.state.formData.name", + "model": { "prop": "name" } + } + }, + "id": "form-name" + } + ], + "id": "item-name" + } + ], + "id": "main-form" + }, + { + "componentName": "div", + "props": { + "className": "form-actions" + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "type": "primary", + "text": "提交", + "onClick": { + "type": "JSExpression", + "value": "this.handleSubmit" + } + }, + "id": "btn-submit" + }, + { + "componentName": "TinyButton", + "props": { + "text": "取消", + "onClick": { + "type": "JSExpression", + "value": "this.handleCancel" + } + }, + "id": "btn-cancel" + } + ], + "id": "actions" + } + ] +} +``` + +--- + +## 常见交互模式 + +> **事件绑定规则**: 事件必须用 `JSExpression` 引用 `methods` 中的方法,不能用 `JSFunction`,`value` 里也不准写函数体。完整 ❌/✅ 见 [SKILL.md](../SKILL.md)「Critical Rules」与 [protocol.md](protocol.md)。下面只给模板。 + +### 事件参数传递 + +方法第一个参数总是 `event`,`params` 中的参数会追加到 event 后面。 + +```json +// 方法定义 - 第一个参数是 event +"methods": { + "deleteItem": { + "type": "JSFunction", + "value": "function(event, itemId) { /* event 和 itemId 都可用 */ }" + } +} + +// 绑定 - params 中的参数追加在 event 后 +"onClick": { + "type": "JSExpression", + "value": "this.deleteItem", + "params": ["123"] // 实际调用: deleteItem(event, 123) +} +``` + +### 传递行数据到事件 + +```json +// 表格列操作 - 传递当前行 +"methods": { + "handleEdit": { + "type": "JSFunction", + "value": "function(event, row) { this.state.editingRow = row; }" + } +} + +// 列定义中使用 slot +{ + "field": "actions", + "title": "操作", + "slots": { + "default": { + "type": "JSSlot", + "params": ["row"], + "value": [ + { + "componentName": "TinyButton", + "props": { + "text": "编辑", + "size": "small", + "onClick": { + "type": "JSExpression", + "value": "this.handleEdit", + "params": ["row"] // handleEdit(event, row) + } + }, + "id": "btn-edit-row" + } + ] + } + } +} +``` + +### 确认删除操作 + +```json +"methods": { + "handleDelete": { + "type": "JSFunction", + "value": "function(event, row) { this.$modal.confirm({ message: '确定要删除吗?' }).then(() => { this.deleteItem(row.id); }); }" + }, + "deleteItem": { + "type": "JSFunction", + "value": "async function(id) { /* 执行删除 */ }" + } +} + +// 绑定 +"onClick": { + "type": "JSExpression", + "value": "this.handleDelete", + "params": ["row"] +} +``` + +### 弹窗表单 + +```json +{ + "componentName": "TinyDialogBox", + "props": { + "visible": { + "type": "JSExpression", + "value": "this.state.dialogVisible" + }, + "title": "编辑", + "width": "600px" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "model": { + "type": "JSExpression", + "value": "this.state.dialogForm" + } + }, + "children": [...], + "id": "dialog-form" + } + ], + "id": "edit-dialog" +} +``` + +### 搜索重置 + +```json +{ + "componentName": "div", + "props": { + "className": "search-actions" + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "type": "primary", + "text": "查询", + "onClick": { + "type": "JSExpression", + "value": "this.handleSearch" + } + }, + "id": "btn-search" + }, + { + "componentName": "TinyButton", + "props": { + "text": "重置", + "onClick": { + "type": "JSExpression", + "value": "this.handleReset" + } + }, + "id": "btn-reset" + } + ], + "id": "search-actions" +} +``` + +--- + +## 布局模式 + +### 页面级 CSS 样式 + +页面可以使用 `css` 字段定义全局样式类。建议使用单行格式以保持 JSON 简洁: + +**推荐 (单行格式)**: + +```json +{ + "componentName": "Page", + "fileName": "MyPage", + "css": ".page-base-style { padding: 24px; background: #FFFFFF; } .block-base-style { margin: 16px; } .component-base-style { margin: 8px; }", + "props": { + "className": "page-base-style" + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "block-base-style" + }, + "children": [...] + } + ] +} +``` + +**备选 (多行格式)** - 用于复杂样式: + +```json +{ + "componentName": "Page", + "fileName": "MyPage", + "css": ".page-base-style {\n padding: 24px;\n background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "props": { + "className": "page-base-style" + } +} +``` + +**注意**: 对于简单样式,使用单行格式。对于复杂样式,使用多行格式并将换行表示为 `\n`。 + +### 内联样式 + +```json +{ + "componentName": "div", + "props": { + "style": "display: flex; align-items: center; justify-content: space-between; padding: 16px; background: #f5f5f5;" + }, + "children": [...], + "id": "styled-container" +} +``` + +### Tailwind CSS 类名 + +支持使用 Tailwind CSS 工具类: + +```json +{ + "componentName": "div", + "props": { + "className": "flex items-center justify-between p-4 bg-gray-100 rounded-lg shadow-md" + }, + "children": [...], + "id": "tailwind-container" +} +``` + +### 左右布局 + +```json +{ + "componentName": "div", + "props": { + "className": "layout-container", + "style": "display: flex; height: 100vh;" + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "layout-sidebar", + "style": "width: 240px; border-right: 1px solid #ddd;" + }, + "children": [...], + "id": "sidebar" + }, + { + "componentName": "div", + "props": { + "className": "layout-main", + "style": "flex: 1; padding: 20px;" + }, + "children": [...], + "id": "main" + } + ], + "id": "layout-container" +} +``` + +### 上下布局 (Header + Content) + +```json +{ + "componentName": "div", + "props": { + "className": "page-layout", + "style": "display: flex; flex-direction: column; height: 100vh;" + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "page-header", + "style": "height: 60px; border-bottom: 1px solid #eee; padding: 0 20px; display: flex; align-items: center;" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "页面标题", + "style": "font-size: 18px; font-weight: bold;" + } + } + ], + "id": "header" + }, + { + "componentName": "div", + "props": { + "className": "page-content", + "style": "flex: 1; padding: 20px; overflow: auto;" + }, + "children": [...], + "id": "content" + } + ], + "id": "layout-root" +} +``` + +### 卡片网格布局 + +```json +{ + "componentName": "div", + "props": { + "className": "card-grid", + "style": "display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px;" + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "card", + "style": "border: 1px solid #ddd; border-radius: 4px; padding: 16px;" + }, + "children": [...], + "id": "card-1" + } + ], + "id": "grid-container" +} +``` + +--- + +## 数据流模式 + +### 数据源加载 + +```json +{ + "state": { + "dataList": [] + }, + "methods": { + "loadData": { + "type": "JSFunction", + "value": "async function() { const data = await this.fetchData('list'); this.state.dataList = data; }" + } + }, + "lifeCycles": { + "onMounted": { + "type": "JSFunction", + "value": "function onMounted() { this.loadData(); }" + } + } +} +``` + +### 键盘事件处理 (Enter 键提交) + +```json +{ + "state": { + "inputText": "" + }, + "methods": { + "handleInputKeyup": { + "type": "JSFunction", + "value": "function(event) { if (event.keyCode === 13) { this.submitForm(event); } }" + }, + "submitForm": { + "type": "JSFunction", + "value": "function(event) { /* 处理提交逻辑 */ }" + } + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "modelValue": { + "type": "JSExpression", + "value": "this.state.inputText", + "model": true + }, + "placeholder": "输入后按Enter提交", + "onKeyup": { + "type": "JSExpression", + "value": "this.handleInputKeyup" + } + }, + "id": "input-submit" + } + ] +} +``` + +### 条件渲染 (v-if) + +```json +{ + "state": { + "isLoading": true, + "hasError": false + }, + "children": [ + { + "componentName": "div", + "props": { + "condition": { + "type": "JSExpression", + "value": "this.state.isLoading" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "Loading..." + } + } + ], + "id": "loading-indicator" + }, + { + "componentName": "div", + "props": { + "condition": { + "type": "JSExpression", + "value": "this.state.hasError" + } + }, + "children": [ + { + "componentName": "TinyAlert", + "props": { + "type": "error", + "title": "加载失败" + } + } + ], + "id": "error-message" + } + ] +} +``` + +### 循环渲染 (v-for) + +```json +{ + "state": { + "items": [ + { "id": 1, "name": "Item 1" }, + { "id": 2, "name": "Item 2" } + ] + }, + "children": [ + { + "componentName": "div", + "props": { + "key": { + "type": "JSExpression", + "value": "index" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": { + "type": "JSExpression", + "value": "item.name" + } + }, + "id": "text-item" + } + ], + "loop": { + "type": "JSExpression", + "value": "this.state.items" + }, + "loopArgs": ["item", "index"], + "id": "item-container" + } + ] +} +``` + +### 动态类名绑定 + +```json +{ + "state": { + "isActive": false, + "size": "large" + }, + "children": [ + { + "componentName": "div", + "props": { + "className": { + "type": "JSExpression", + "value": "['base-class', {'active': this.state.isActive, 'large': this.state.size === 'large'}]" + } + }, + "id": "dynamic-class-div" + } + ] +} +``` + +### 表格行操作 + +```json +{ + "componentName": "TinyGrid", + "props": { + "columns": [ + { + "field": "actions", + "title": "操作", + "slots": { + "default": { + "type": "JSSlot", + "params": ["row"], + "value": [ + { + "componentName": "TinyButton", + "props": { + "text": "编辑", + "size": "small", + "onClick": { + "type": "JSExpression", + "value": "this.handleEdit", + "params": ["row"] + } + }, + "id": "btn-edit" + } + ] + } + } + } + ] + } +} +``` + +### 父子组件通信 + +```json +// 父组件 +{ + "componentName": "div", + "children": [ + { + "componentName": "ChildBlock", + "props": { + "data": { + "type": "JSExpression", + "value": "this.state.parentData" + }, + "onUpdate": { + "type": "JSExpression", + "value": "this.handleChildUpdate" + } + }, + "id": "child-block" + } + ] +} + +// 子组件(区块) +{ + "componentName": "Block", + "fileName": "ChildBlock", + "props": {}, + "methods": { + "emitChange": { + "type": "JSFunction", + "value": "function(newValue) { this.emit('update', newValue); }" + } + } +} +``` + +--- + +## 区块设计模式 + +### 可配置区块 + +```json +{ + "componentName": "Block", + "fileName": "ConfigurableBlock", + "schema": { + "properties": [ + { + "label": { "zh_CN": "基础信息" }, + "content": [ + { + "property": "title", + "type": "String", + "defaultValue": "默认标题", + "label": { + "text": { "zh_CN": "标题" } + }, + "widget": { + "component": "MetaInput", + "props": {} + } + }, + { + "property": "showFooter", + "type": "Boolean", + "defaultValue": true, + "label": { + "text": { "zh_CN": "显示底部" } + }, + "widget": { + "component": "MetaSwitch", + "props": {} + } + } + ] + } + ], + "events": { + "onConfirm": { + "label": { "zh_CN": "确认事件" } + } + } + }, + "methods": { + "handleConfirm": { + "type": "JSFunction", + "value": "function(event) { this.emit('confirm'); }" + } + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "block-content" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": { + "type": "JSExpression", + "value": "this.props.title" + } + } + } + ], + "id": "block-content" + }, + { + "componentName": "div", + "props": { + "className": "block-footer", + "condition": { + "type": "JSExpression", + "value": "this.props.showFooter" + } + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "type": "primary", + "text": "确认", + "onClick": { + "type": "JSExpression", + "value": "this.handleConfirm" + } + } + } + ] + } + ] +} +``` + +### 带状态管理的区块 + +```json +{ + "componentName": "Block", + "fileName": "StatefulBlock", + "state": { + "localData": [], + "loading": false + }, + "methods": { + "fetchData": { + "type": "JSFunction", + "value": "async function() { this.state.loading = true; const data = await this.props.dataSource(); this.state.localData = data; this.state.loading = false; }" + } + }, + "lifeCycles": { + "setup": { + "type": "JSFunction", + "value": "function setup({ props, watch, onMounted }) { watch(() => props.url, () => { this.fetchData(); }); }" + } + }, + "children": [ + { + "componentName": "TinyGrid", + "props": { + "data": { + "type": "JSExpression", + "value": "this.state.localData" + }, + "loading": { + "type": "JSExpression", + "value": "this.state.loading" + } + } + } + ] +} +``` + +### Watch 监听器 (响应式数据) + +使用 `setup` 生命周期中的 `watch` 来监听数据变化: + +```json +{ + "state": { + "userId": "123", + "userDetails": null + }, + "methods": { + "loadUserDetails": { + "type": "JSFunction", + "value": "async function(id) { /* 加载用户详情 */ }" + } + }, + "lifeCycles": { + "setup": { + "type": "JSFunction", + "value": "function setup({ state, watch }) {\n watch(() => state.userId, (newId, oldId) => {\n if (newId !== oldId) {\n this.loadUserDetails(newId);\n }\n });\n}" + } + } +} +``` + +**Watch 深度监听**: + +```json +"setup": { + "type": "JSFunction", + "value": "function setup({ props, watch }) {\n watch(() => props.list, (list) => {\n // 列表变化时执行\n }, { deep: true });\n}" +} +``` + +### 方法间相互调用 + +```json +{ + "methods": { + "handleButtonClick": { + "type": "JSFunction", + "value": "function handleButtonClick(event) {\n console.log('button click');\n this.test('test param');\n}" + }, + "test": { + "type": "JSFunction", + "value": "function test(name) {\n console.log('test', name);\n}" + } + } +} +``` + +### 对话框确认操作 + +```json +{ + "state": { + "dialogVisible": false + }, + "methods": { + "showConfirm": { + "type": "JSFunction", + "value": "function(event) { this.$modal.confirm({ message: '确定要执行此操作吗?' }).then(() => { this.doAction(); }); }" + }, + "doAction": { + "type": "JSFunction", + "value": "async function() { /* 执行操作 */ }" + } + } +} +``` + +### 消息提示 + +```json +{ + "methods": { + "showSuccess": { + "type": "JSFunction", + "value": "function(event) { this.$modal.message({ message: '操作成功!', status: 'success' }); }" + }, + "showError": { + "type": "JSFunction", + "value": "function(event, msg) { this.$modal.message({ message: msg || '操作失败', status: 'error' }); }" + } + } +} +``` + +### 数组操作模式 + +```json +{ + "state": { + "items": [{ "id": 1, "text": "Item 1" }] + }, + "methods": { + "addItem": { + "type": "JSFunction", + "value": "function(event, text) { this.state.items.push({ id: Date.now(), text: text }); }" + }, + "removeItem": { + "type": "JSFunction", + "value": "function(event, id) { this.state.items = this.state.items.filter(item => item.id !== id); }" + }, + "updateItem": { + "type": "JSFunction", + "value": "function(event, id, newText) { const item = this.state.items.find(i => i.id === id); if (item) { item.text = newText; } }" + } + } +} +``` + +``` + +``` diff --git a/.agents/skills/tinyengine-dsl-generator/references/protocol.md b/.agents/skills/tinyengine-dsl-generator/references/protocol.md new file mode 100644 index 0000000000..28c5b70e49 --- /dev/null +++ b/.agents/skills/tinyengine-dsl-generator/references/protocol.md @@ -0,0 +1,595 @@ +# 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 // 常量 + 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 // 页面状态 + methods?: Record // 页面方法 + lifeCycles?: Record // 生命周期 + 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 | 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. 第一个参数自动是 event,params 中的参数追加在后面 +// 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 // 区块状态 + methods?: Record // 区块方法 + lifeCycles?: Record // 生命周期 + schema: IBlockSchemaConfig // 区块对外暴露的配置schema + children?: IComponentSchema[] // 区块内容 + dataSource?: IDataSource // 区块数据源 +} + +interface IBlockSchemaConfig { + properties: IPropertyConfig[] // 可配置属性 + events?: Record // 可触发事件 + slots?: Record // 插槽定义 +} + +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": {} + } + ] +} +``` + +生成代码:`` + +### 使用作用域插槽 + +```json +{ + "componentName": "template", + "props": { + "slot": { + "name": "footer", + "params": ["row"] + } + }, + "children": [...] +} +``` + +生成代码:`` + +### 表格插槽示例 + +```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 + data?: Array + dataHandler?: IJSFunction + errorHandler?: IJSFunction + option?: { + method: string + url: string + } + shouldFetch?: IJSFunction + willFetch?: IJSFunction + } + value?: { + data?: Array + columns?: Array + } +} +``` + +### 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" + } + } + } +} +``` diff --git a/.agents/skills/tinyengine-dsl-generator/scripts/check_css.mjs b/.agents/skills/tinyengine-dsl-generator/scripts/check_css.mjs new file mode 100644 index 0000000000..5613f94077 --- /dev/null +++ b/.agents/skills/tinyengine-dsl-generator/scripts/check_css.mjs @@ -0,0 +1,179 @@ +#!/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 [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(); +} diff --git a/.agents/skills/tinyengine-dsl-generator/scripts/check_event_bindings.mjs b/.agents/skills/tinyengine-dsl-generator/scripts/check_event_bindings.mjs new file mode 100644 index 0000000000..684f3f55f6 --- /dev/null +++ b/.agents/skills/tinyengine-dsl-generator/scripts/check_event_bindings.mjs @@ -0,0 +1,185 @@ +#!/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 '); + 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(); +} diff --git a/.agents/skills/tinyengine-dsl-generator/scripts/query_components.mjs b/.agents/skills/tinyengine-dsl-generator/scripts/query_components.mjs new file mode 100644 index 0000000000..aa3ebf72de --- /dev/null +++ b/.agents/skills/tinyengine-dsl-generator/scripts/query_components.mjs @@ -0,0 +1,215 @@ +#!/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= 环境变量覆盖 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= 覆盖)`); + } 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(); diff --git a/.agents/skills/tinyengine-dsl-generator/scripts/validate_all.sh b/.agents/skills/tinyengine-dsl-generator/scripts/validate_all.sh new file mode 100755 index 0000000000..cb91707c4c --- /dev/null +++ b/.agents/skills/tinyengine-dsl-generator/scripts/validate_all.sh @@ -0,0 +1,43 @@ +#!/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 " + 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 "======================================" diff --git a/.agents/skills/tinyengine-dsl-generator/scripts/validate_dsl.mjs b/.agents/skills/tinyengine-dsl-generator/scripts/validate_dsl.mjs new file mode 100644 index 0000000000..c0ad924bcc --- /dev/null +++ b/.agents/skills/tinyengine-dsl-generator/scripts/validate_dsl.mjs @@ -0,0 +1,306 @@ +#!/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 [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(); +} diff --git a/.agents/skills/tinyengine-dsl-generator/scripts/validate_page.mjs b/.agents/skills/tinyengine-dsl-generator/scripts/validate_page.mjs new file mode 100644 index 0000000000..5a22447de5 --- /dev/null +++ b/.agents/skills/tinyengine-dsl-generator/scripts/validate_page.mjs @@ -0,0 +1,220 @@ +#!/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 文件;失败时抛出 SyntaxError(JSON)或带 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 '); + 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(); diff --git a/.gitignore b/.gitignore index daff07cbe6..94ac98cace 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ pnpm-debug.log* *.sw? tmp temp +__pycache__ + +# .claude/skills is a generated link to .agents/skills (see scripts/link-skills.js) +.claude/skills diff --git a/docs/advanced-features/using-skill-to-integrate-local-ai-agent.md b/docs/advanced-features/using-skill-to-integrate-local-ai-agent.md new file mode 100644 index 0000000000..e9352772fa --- /dev/null +++ b/docs/advanced-features/using-skill-to-integrate-local-ai-agent.md @@ -0,0 +1,365 @@ +# 对接本地 AI Agent 生成页面或应用 + +## 概述 + +TinyEngine 现在支持将 **File 存储模式** 与一份标准化的 **DSL Skill** 结合,让本地 AI Agent(如 Claude Code、Cursor 等)能够直接生成符合 TinyEngine 协议的页面 / 应用 / 区块 JSON 文件,并立即被设计器加载和编辑。 + +这套能力解决了一个核心痛点:**如何让 AI Agent 可靠地产出 TinyEngine 能“看懂”的 DSL**。过去,直接让 AI 生成 Schema 容易出现事件绑定类型错误、`occupier` 非空导致不可编辑、`model` 未开启导致输入框失效等问题。Skill 把 TinyEngine 的协议规范、组件清单、常见模式和校验规则固化为一份 Agent 可读取的指令集,从而把“自由发挥的 AI”约束成“按规范出码的生成器”。 + +> 本文面向**希望用本地 AI Agent 批量、可控地生成 TinyEngine 页面或应用**的开发者。如果你只想在浏览器里用平台内置 AI 辅助搭建单个页面,请参考[新版AI插件使用](./new-ai-plugin-usage.md)。 + +## 背景与价值 + +这套方案由两块能力拼合而成: + +| 能力 | 作用 | +| --- | --- | +| **File 存储模式** | MockServer 以纯文本 JSON 文件读写数据(`mockServer/data/`),文件即数据源,AI / 人 / Git 都能直接操作 | +| **DSL Skill** | 一份标准化指令集(`SKILL.md` + 参考文档 + 校验脚本),约束 AI Agent 产出符合 TinyEngine 协议的 DSL | + +二者协同带来的价值: + +- **协议一致**:Skill 内置 DSL 协议、组件清单与常见陷阱,让 AI 生成的 JSON 开箱即用。 +- **可直接编辑**:生成的文件落盘到 File 模式目录后,设计器自动识别,且 `occupier` 为 `null`,进入画布即可二次编辑。 +- **版本控制友好**:格式化的 JSON 可直接提交 Git,页面 / 应用的变更历史一目了然。 +- **批量与可复用**:适合一次性生成整个应用(多页面 + 物料 + 关联),也可沉淀为团队模板。 + +> MockServer File 模式当前只支持Fork代码仓开发场景。如果是使用CLI创建全新的项目,暂无法使用全部的两块能力,但可以单独使用DSL Skill生成JSON格式Schema到指定目录(需要复制Skill到新项目Agent的Skill目录,如.agents/skills/)。 + + +## 工作原理 + +整体链路如下: + +``` + 自然语言 / 截图描述 + │ + ▼ + ┌───────────────────────┐ + │ 本地 AI Agent │ ◄── 读取 .agents/skills/tinyengine-dsl-generator + │ (Claude Code/Cursor) │ (SKILL.md + references + scripts) + └───────────────────────┘ + │ 按 Skill 规范生成并校验 + ▼ + 符合协议的 DSL JSON 文件 + mockServer/data/{pages,apps,blocks}/.json + │ + ▼ + ┌───────────────────────┐ + │ MockServer (File 模式) │ ◄── 启动命令:pnpm dev:file + └───────────────────────┘ + │ + ▼ + TinyEngine 设计器加载 / 渲染 / 可编辑 +``` + +几个关键约定(生成时务必遵守,详见后文 [Skill 中的关键规则](#skill-中的关键规则速查)): + +- **文件即数据**:File 模式下,MockServer 直接扫描 `mockServer/data/` 目录的 JSON 文件,文件名优先取 `name || label`(冲突时自动追加后缀)。 +- **可编辑性**:页面 / 区块的 `occupier` 必须为 `null`,否则进入画布后会提示被占用而无法编辑。 +- **命名与关联**:页面通过 `app` 字段关联所属应用,`route` 需在应用内唯一;应用 ID 使用整数,Page 文件的 `app` 引用使用字符串。 + +## 前置条件 + +1. 已安装仓库依赖(`pnpm install`)。 +2. 本地能正常启动 MockServer 与设计器。 +3. 仓库中已包含 DSL Skill:`.agents/skills/tinyengine-dsl-generator`(详情见下一节)。 + +## 准备 Skill(让 AI Agent 感知到它) + +DSL Skill 的规范位置在仓库内的: + +``` +.agents/skills/tinyengine-dsl-generator/ +├── SKILL.md # 主指令:生成流程、结构、关键规则、常见陷阱 +├── references/ +│ ├── protocol.md # DSL 协议完整规范(含 TypeScript 接口) +│ ├── components.md # 可用组件清单与 props/events +│ └── patterns.md # 列表页 / 表单页等常见模板与交互模式 +└── scripts/ + ├── validate_dsl.mjs # 结构校验 + ├── check_event_bindings.mjs + ├── check_css.mjs + └── validate_all.sh # 一键综合校验 +``` + +### Claude Code/Cursor/OpenCode/Codex(开箱即用) + +Cursor/OpenCode/Codex等多数Agent会自动识别.agents/skills目录中的技能,对于Claude Code,已将 `.claude/skills` 软链到 `.agents/skills`,因此在仓库内启动 Claude Code/Cursor/OpenCode/Codex 时,`tinyengine-dsl-generator` Skill 会被**自动发现并加载**,无需额外配置。直接用自然语言描述目标即可,Agent 会主动调用该 Skill。 + +### 其他本地 Agent + +如果你的 Agent 不自动扫描 `.agents/skills`,只需在对话中**显式指向 `SKILL.md`**,例如: + +```text +请阅读 .agents/skills/tinyengine-dsl-generator/SKILL.md 及其 references/ 目录, +按照其中的协议规范,帮我生成一个 TinyEngine 登录页面,并写入到 +mockServer/data/pages/Login.json。 +``` + +Agent 读取 `SKILL.md` 后,即可获得完整的生成流程、结构定义与校验规则。 + +## 使用流程 + +### 1. 启动 File 模式的 MockServer + +```bash +pnpm dev:file # 同时启动设计器前端 与 File模式的MockServer +# 或者单独启动 mock server +pnpm serve:backend:file +``` + +数据将读写 `mockServer/data/` 目录。 + +### 2. 向 AI Agent 描述目标 + +明确告诉 Agent 你要生成什么,越具体越好。例如: + +- “生成一个登录页面,包含用户名、密码输入框和登录按钮,点击登录校验非空后跳转。” +- “生成一个用户管理列表页,顶部搜索表单 + 表格 + 分页,表格带状态列和操作列。” +- “基于这张设计稿截图生成页面。”(部分 Agent 支持图片输入) + +### 3. Agent 按 Skill 生成 DSL + +Agent 会遵循 Skill 内定义的标准流程: + +1. **理解目标**:判断产物类型 —— 单页面(Page)、可复用区块(Block)还是完整应用(App,多页面 + 物料 + meta)。 +2. **收集需求**:组件层级、状态、事件、数据源;区块还要收集对外暴露的 props;应用还要收集页面清单与共享物料。 +3. **查阅参考**:按需对照 `references/protocol.md`(结构)、`references/components.md`(组件 props/events)、`references/patterns.md`(模板)。 +4. **生成 DSL**:严格按协议结构产出 JSON。 +5. **自检与校验**:套用 Skill 内的「生成前检查清单」,并运行校验脚本。 +6. **落盘**:写入对应目录(见下节)。 + +### 4. 在设计器中查看与编辑 + +文件写入后,回到运行中的设计器: + +- 确保当前应用与页面 `app` 字段一致; +- 在页面列表中即可看到新生成的页面,点击进入画布; +- 因为 `occupier` 为 `null`,画布可直接二次编辑、保存。 + +### 5. 持续迭代与版本管理 + +- 继续用自然语言迭代:“把表格的列宽调大”“增加一个重置按钮”,Agent 会更新对应 JSON。 +- 生成的 JSON 可直接 `git add` 提交,团队成员通过 Git 查看页面配置的演进。 +- 若需迁回 DB 模式或发布到远端 DB 环境,可使用 `npm run export-db-to-file` 等迁移脚本互转(两模式数据格式完全兼容)。 + +## 生成产物说明 + +### 文件输出路径 + +| 产物 | 文件路径 | 文件名规则 | +| --- | --- | --- | +| 应用 | `mockServer/data/apps/.json` | 应用 `name` | +| 页面 | `mockServer/data/pages/.json` | 页面 `name` | +| 区块 | `mockServer/data/blocks/.json` | 区块 `label` | + +### Page 文件结构(外层包装 + 内层 DSL) + +`page_content` 内才是 `componentName: "Page"` 的真正页面 DSL。 + +```json +{ + "name": "Login", + "id": "a1b2c3d4e5f6g7h8", + "app": "1", + "route": "Login", + "page_content": { + "componentName": "Page", + "fileName": "Login", + "props": { "className": "login-page" }, + "css": ".login-page { display: flex; justify-content: center; padding: 40px; }", + "state": { "form": { "username": "", "password": "" } }, + "methods": {}, + "lifeCycles": {}, + "children": [], + "dataSource": { "list": [] } + }, + "tenant": 1, + "parentId": "0", + "isPage": true, + "group": "staticPages", + "occupier": null, + "isHome": false +} +``` + +### Block 文件结构 + +`content` 内是 `componentName: "Block"` 的区块 DSL,`schema.properties` 描述对外可配置的 props。 + +```json +{ + "id": "BlockUniqueId1234567", + "label": "PortalBlock", + "framework": "Vue", + "path": "portal", + "content": { + "componentName": "Block", + "fileName": "PortalBlock", + "props": {}, + "state": {}, + "methods": {}, + "children": [], + "schema": { "properties": [], "events": {}, "slots": {} } + }, + "public": 1, + "is_published": true, + "occupier": null +} +``` + +### App 文件结构要点 + +应用文件主要承载元信息,关键字段: + +- `id`:应用 ID(Skill 推荐整数,如 `918`),供页面通过 `app` 字段引用; +- `name`:应用名,同时决定文件名; +- `framework`:通常为 `"Vue"`; +- `created_at` / `updated_at`:ISO 时间字符串。 + +### 命名与关联约定 + +- 文件名优先使用记录的 `name || label`;同名冲突时 MockServer 会自动追加随机后缀。 +- 页面的 `route` 需在所属应用内唯一,否则会被唯一性约束拦截。 +- 页面的 `app` 字段必须以字符串形式引用所属应用的 `id`,例如应用 `id` 为 `1` 时页面写 `"app": "1"`。 + +## 示例:生成一个登录页 + +向 Agent 输入: + +```text +请用 tinyengine-dsl-generator Skill 生成一个登录页面: +- 用户名输入框、密码输入框、登录按钮 +- 点击登录时校验非空 +- 写入到 mockServer/data/pages/Login.json,并校验通过 +``` + +Agent 产出的页面核心 DSL(节选): + +```json +{ + "name": "Login", + "id": "a1b2c3d4e5f6g7h8", + "app": "1", + "route": "Login", + "page_content": { + "componentName": "Page", + "fileName": "Login", + "props": { "className": "login-page" }, + "state": { "form": { "username": "", "password": "" } }, + "methods": { + "handleSubmit": { + "type": "JSFunction", + "value": "function(event) { if (!this.state.form.username || !this.state.form.password) { return; } /* 提交逻辑 */ }" + } + }, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyInput", + "id": "input-username", + "props": { + "placeholder": "请输入用户名", + "modelValue": { "type": "JSExpression", "value": "this.state.form.username", "model": true } + } + }, + { + "componentName": "TinyInput", + "id": "input-password", + "props": { + "placeholder": "请输入密码", + "modelValue": { "type": "JSExpression", "value": "this.state.form.password", "model": true } + } + }, + { + "componentName": "TinyButton", + "id": "btn-login", + "props": { + "text": "登录", + "type": "primary", + "onClick": { "type": "JSExpression", "value": "this.handleSubmit" } + } + } + ], + "dataSource": { "list": [] } + }, + "tenant": 1, + "parentId": "0", + "isPage": true, + "group": "staticPages", + "occupier": null, + "isHome": false +} +``` + +关键点回顾:输入框用 `model: true` 实现双向绑定;按钮 `onClick` 用 `JSExpression` 引用 `methods` 中的方法;`occupier` 为 `null` 保证可编辑。 + +## DSL产物校验 + +Skill 自带校验脚本,通过校验可以避免把不规范的结构带入画布。 + +一键综合校验(结构 + 事件绑定 + CSS): + +```bash +bash .agents/skills/tinyengine-dsl-generator/scripts/validate_all.sh mockServer/data/pages/Login.json +``` + +也可以单独运行: + +```bash +# 结构校验(必填字段、componentName、meta、ID 等) +node .agents/skills/tinyengine-dsl-generator/scripts/validate_dsl.mjs mockServer/data/pages/Login.json + +# 事件绑定检查(确保用 JSExpression 引用方法) +node .agents/skills/tinyengine-dsl-generator/scripts/check_event_bindings.mjs mockServer/data/pages/Login.json + +# CSS 语法检查 +node .agents/skills/tinyengine-dsl-generator/scripts/check_css.mjs mockServer/data/pages/Login.json basic +``` + +此外,Skill 内置一份**生成前检查清单(Pre-Generation Checklist)**,Agent 会在落盘前逐条核对,主要包括:事件绑定全部用 `JSExpression`、方法首参为 `event`、`modelValue` 带 `model: true`、应用与页面 ID 统一为整数、生命周期名以 `on` 开头、`occupier` 为 `null`、组件 ID 唯一、CSS 类名用 `className`。完整清单见 [SKILL.md](https://github.com/opentiny/tiny-engine/blob/develop/.agents/skills/tinyengine-dsl-generator/SKILL.md)。 + +## Skill 中的关键规则(速查) + +| 场景 | 正确做法 | 常见错误 | +| --- | --- | --- | +| 事件绑定 | `JSExpression` 引用方法:`{ "type": "JSExpression", "value": "this.handleSubmit" }` | 用 `JSFunction` 写内联函数 | +| 方法参数 | 方法签名首参恒为 `event`,额外参数经 `params` 追加 | 漏掉 `event` 导致取参错位 | +| 双向绑定 | `modelValue` 带 `"model": true` | 用 `model: { prop: ... }` 或省略,输入框不生效 | +| 生命周期 | 名称以 `on` 开头(`onMounted`),用带函数名的完整函数体 | 用 `mounted` 或匿名函数 | +| 可编辑性 | `"occupier": null` | 写入用户对象,画布提示被占用 | +| CSS 类名 | 使用 `className` | 使用 `class` | +| 组件 ID | 全局唯一 | 复用 ID 导致渲染/选中异常 | +| 保留名 | 禁止自定义组件叫 `Page`/`Block`/`Text`/`Template`/`Slot`/`Collection` | 与内置容器冲突 | + +## 常见问题 + +### 页面在设计器中不可编辑 / 提示被占用 + +检查页面 / 区块 JSON 的 `occupier` 字段,必须为 `null`。AI 直接落盘的文件若带了占用者信息会导致无法进入编辑。 + +### 输入框无法输入 / 数据不回填 + +确认输入组件的 `modelValue` 绑定带了 `"model": true`,且引用的 `state` 变量已在 `state` 中声明。 + +### 按钮点击无反应 / 事件不触发 + +事件绑定(`onClick`/`onChange`/`onKeyup` 等)必须用 `JSExpression` 引用 `methods` 中定义的方法,且方法名正确。不要把函数定义直接写在事件绑定的 `value` 里。 + +### 设计器里看不到新生成的页面 + +依次排查: + +1. MockServer 是否以 `MOCK_DB_MODE=file` 启动; +2. 文件是否在 `mockServer/data/pages/` 下,且文件名与记录 `name` 一致; +3. 页面的 `app` 字段是否与当前应用的 `id` 匹配; +4. `route` 是否与同应用内已有页面重复被唯一性约束拦截。 + +### 想让团队复用生成的页面 / 应用 + +直接把 `mockServer/data/` 下的 JSON 提交到 Git。其他人拉取后以 File 模式启动即可看到同样的页面配置与变更历史。需要迁回 DB 模式时,使用迁移脚本即可(两模式格式兼容)。 + +## 相关文档 + +- [DSL Skill](https://github.com/opentiny/tiny-engine/blob/develop/.agents/skills/tinyengine-dsl-generator/SKILL.md) diff --git a/docs/catalog.json b/docs/catalog.json index 97e64f383d..d926ca4f6e 100644 --- a/docs/catalog.json +++ b/docs/catalog.json @@ -100,6 +100,10 @@ "title": "条件渲染", "name": "conditional-rendering.md" }, + { + "title": "对接本地 AI Agent 生成页面或应用", + "name": "using-skill-to-integrate-local-ai-agent.md" + }, { "title": "新版AI插件使用", "name": "new-ai-plugin-usage.md" diff --git a/eslint.config.mjs b/eslint.config.mjs index 29d90a357d..5ac1de4234 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -67,7 +67,7 @@ export default defineConfigWithVueTs( } }, { - files: ['scripts/**/*'], + files: ['scripts/**/*', '.agents/skills/**/*', '.claude/skills/**/*'], rules: { 'no-console': 'off', '@typescript-eslint/no-require-imports': 'off' diff --git a/mockServer/data/apps/dashboard.json b/mockServer/data/apps/dashboard.json new file mode 100644 index 0000000000..a4e7996c62 --- /dev/null +++ b/mockServer/data/apps/dashboard.json @@ -0,0 +1,50 @@ +{ + "id": 16, + "createdBy": "6", + "lastUpdatedBy": "6", + "tenantId": "1", + "renterId": null, + "siteId": null, + "name": "dashboard", + "appWebsite": null, + "platformHistoryId": "1", + "publishUrl": null, + "editorUrl": null, + "visitUrl": null, + "assetsUrl": "template-cover-1", + "state": null, + "homePage": null, + "css": null, + "config": {}, + "constants": null, + "dataHandler": {}, + "description": "数据看板", + "latest": null, + "gitGroup": null, + "projectName": null, + "branch": null, + "isDemo": null, + "isDefault": null, + "templateType": null, + "isTemplate": null, + "industryId": null, + "sceneId": null, + "setTemplateTime": null, + "setTemplateBy": null, + "setDefaultBy": null, + "framework": "Vue", + "defaultLang": null, + "extendConfig": {}, + "dataHash": null, + "canAssociate": null, + "industry": [], + "scene": [], + "created_at": "2026-01-15 02:35:32", + "updated_at": "2026-01-15 02:35:32", + "platform": 1, + "image_url": null, + "published": false, + "global_state": [], + "data_source_global": {}, + "_id": "epHKwpqjCPamtee4" +} \ No newline at end of file diff --git a/mockServer/data/apps/portal-app.json b/mockServer/data/apps/portal-app.json new file mode 100644 index 0000000000..b3829c9f40 --- /dev/null +++ b/mockServer/data/apps/portal-app.json @@ -0,0 +1,153 @@ +{ + "id": 1, + "createdBy": "1", + "lastUpdatedBy": "1", + "tenantId": "1", + "renterId": null, + "siteId": "1", + "name": "portal-app", + "appWebsite": null, + "platformHistoryId": "1", + "publishUrl": null, + "editorUrl": null, + "visitUrl": null, + "assetsUrl": null, + "state": null, + "homePage": 0, + "css": null, + "config": {}, + "constants": null, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHanlder(res){\n return res;\n}" + }, + "description": "demo应用", + "latest": "22", + "gitGroup": null, + "projectName": null, + "branch": "develop", + "isDemo": null, + "isDefault": null, + "templateType": "serviceDevelop", + "isTemplate": null, + "industryId": null, + "sceneId": null, + "setTemplateTime": "2023-11-19T18:14:37", + "setTemplateBy": "1", + "setDefaultBy": "1", + "framework": "Vue", + "defaultLang": null, + "extendConfig": { + "business": { + "serviceName": "", + "endpointName": "cce", + "endpointId": "ee", + "serviceId": "ee", + "router": "ee" + }, + "env": { + "alpha": { + "regions": [ + { + "name": "", + "baseUrl": "", + "isDefault": false + } + ], + "isDefault": true + } + }, + "type": "console" + }, + "dataHash": "8b0eba6ad055532a586f9f669108fabb", + "canAssociate": "1", + "industry": [], + "scene": [], + "created_at": "2024-10-16 23:27:10", + "updated_at": "2024-10-16 23:27:10", + "platform": 1, + "image_url": null, + "published": false, + "global_state": [ + { + "id": "test2", + "state": { + "name1": "xxx1" + }, + "getters": { + "count": { + "type": "JSFunction", + "value": "function count() {}" + } + }, + "actions": { + "actions": { + "type": "JSFunction", + "value": "function actions() {}" + } + } + }, + { + "id": "test3", + "state": { + "name1": "xxx" + }, + "getters": { + "count": { + "type": "JSFunction", + "value": "function count() {}" + } + }, + "actions": { + "actions": { + "type": "JSFunction", + "value": "function actions() {}" + } + } + }, + { + "id": "test4", + "state": { + "region": "", + "scenario": "all", + "productId": "", + "planId": "", + "addEvs": false, + "addHss": false, + "addCbr": false, + "period": { + "value": 1, + "unit": "month" + }, + "amount": 1 + }, + "getters": {}, + "actions": {} + }, + { + "id": "test1", + "state": { + "name1": "xxx" + }, + "getters": { + "count": { + "type": "JSFunction", + "value": "function count() {}" + } + }, + "actions": { + "actions": { + "type": "JSFunction", + "value": "function actions() {}" + } + } + } + ], + "data_source_global": { + "dataHandler": { + "type": "JSFunction", + "value": "function dataHanlder(res){\n return res;\n}" + } + }, + "_id": "fBJD5wIbfFVsUdiQ" +} \ No newline at end of file diff --git a/mockServer/data/appsSchema/1.json b/mockServer/data/appsSchema/1.json new file mode 100644 index 0000000000..32a4bdb8cf --- /dev/null +++ b/mockServer/data/appsSchema/1.json @@ -0,0 +1,1362 @@ +{ + "id": 1, + "meta": { + "name": "portal-app", + "tenant": 1, + "git_group": "", + "project_name": "", + "description": "demo应用", + "branch": "develop", + "is_demo": null, + "global_state": [], + "appId": "1", + "creator": "", + "gmt_create": "2022-06-08 03:19:01", + "gmt_modified": "2023-08-23 10:22:28" + }, + "dataSource": { + "list": [ + { + "id": 132, + "name": "getAllComponent", + "data": { + "data": [], + "type": "array" + }, + "tpl": null, + "app": "1", + "desc": null, + "created_at": "2022-06-28T06:26:26.000Z", + "updated_at": "2022-06-28T07:02:30.000Z" + }, + { + "id": 133, + "name": "getAllList", + "data": { + "columns": [ + { + "name": "test", + "title": "测试", + "field": "test", + "type": "string", + "format": {} + }, + { + "name": "test1", + "title": "测试1", + "field": "test1", + "type": "string", + "format": {} + } + ], + "type": "array", + "data": [ + { + "test": "test1", + "test1": "test1", + "_id": "341efc48" + }, + { + "test": "test2", + "test1": "test1", + "_id": "b86b516c" + }, + { + "test": "test3", + "test1": "test1", + "_id": "f680cd78" + } + ], + "options": { + "uri": "", + "method": "GET" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(data) { \n return data \n}" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option \n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {}" + } + }, + "tpl": null, + "app": "1", + "desc": null, + "created_at": "2022-06-28T07:32:16.000Z", + "updated_at": "2023-01-19T03:29:11.000Z" + }, + { + "id": 135, + "name": "getAllMaterialList", + "data": { + "columns": [ + { + "name": "id", + "title": "id", + "field": "id", + "type": "string", + "format": {} + }, + { + "name": "name", + "title": "name", + "field": "name", + "type": "string", + "format": {} + }, + { + "name": "framework", + "title": "framework", + "field": "framework", + "type": "string", + "format": { + "required": true + } + }, + { + "name": "components", + "title": "components", + "field": "components", + "type": "string", + "format": {} + }, + { + "name": "content", + "title": "content", + "field": "content", + "type": "string", + "format": {} + }, + { + "name": "url", + "title": "url", + "field": "url", + "type": "string", + "format": {} + }, + { + "name": "published_at", + "title": "published_at", + "field": "published_at", + "type": "string", + "format": {} + }, + { + "name": "created_at", + "title": "created_at", + "field": "created_at", + "type": "string", + "format": {} + }, + { + "name": "updated_at", + "title": "updated_at", + "field": "updated_at", + "type": "string", + "format": {} + }, + { + "name": "published", + "title": "published", + "field": "published", + "type": "string", + "format": {} + }, + { + "name": "last_build_info", + "title": "last_build_info", + "field": "last_build_info", + "type": "string", + "format": {} + }, + { + "name": "tenant", + "title": "tenant", + "field": "tenant", + "type": "string", + "format": {} + }, + { + "name": "version", + "title": "version", + "field": "version", + "type": "string", + "format": {} + }, + { + "name": "description", + "title": "description", + "field": "description", + "type": "string", + "format": {} + } + ], + "type": "array", + "data": [ + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "2a23e653" + }, + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "06b253be" + }, + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "c55a41ed" + }, + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "f37123ec" + }, + { + "id": "7a63c1a2", + "url": "", + "name": "tiny-vue", + "tenant": "", + "content": "Tiny Vue物料", + "version": "1.0.0", + "framework": "Vue", + "published": "", + "components": "", + "created_at": "", + "updated_at": "", + "description": "Tiny Vue物料", + "published_at": "", + "last_build_info": "", + "_id": "7a63c1a2" + } + ], + "options": { + "uri": "", + "method": "GET" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option \n}" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(data) { \n return data \n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {}" + } + }, + "tpl": null, + "app": "1", + "desc": null, + "created_at": "2022-06-29T00:57:50.000Z", + "updated_at": "2023-05-15T02:37:12.000Z" + }, + { + "id": 139, + "name": "treedata", + "data": { + "data": [ + { + "label": "level111", + "value": "111", + "id": "f6609643", + "pid": "", + "_RID": "row_4" + }, + { + "label": "level1-son", + "value": "111-1", + "id": "af1f937f", + "pid": "f6609643", + "_RID": "row_5" + }, + { + "label": "level222", + "value": "222", + "id": "28e3709c", + "pid": "", + "_RID": "row_6" + }, + { + "label": "level2-son", + "value": "222-1", + "id": "6b571bef", + "pid": "28e3709c", + "_RID": "row_5" + }, + { + "id": "6317c2cc", + "pid": "fdfa", + "label": "fsdfaa", + "value": "fsadf", + "_RID": "row_6" + }, + { + "id": "9cce369f", + "pid": "test", + "label": "test1", + "value": "001" + } + ], + "type": "tree" + }, + "tpl": null, + "app": "1", + "desc": null, + "created_at": "2022-06-30T06:13:57.000Z", + "updated_at": "2022-07-29T03:14:55.000Z" + }, + { + "id": 150, + "name": "componentList", + "data": { + "data": [ + { + "_RID": "row_1", + "name": "表单", + "isSelected": "true", + "description": "由按钮、输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据" + }, + { + "name": "按钮", + "isSelected": "false", + "description": "常用的操作按钮,提供包括默认按钮、图标按钮、图片按钮、下拉按钮等类型" + }, + { + "id": "490f8a00", + "_RID": "row_3", + "name": "表单项", + "framework": "", + "materials": "", + "description": "Form 组件下的 FormItem 配置" + }, + { + "id": "c259b8b3", + "_RID": "row_4", + "name": "开关", + "framework": "", + "materials": "", + "description": "关闭或打开" + }, + { + "id": "083ed9c7", + "_RID": "row_5", + "name": "互斥按钮组", + "framework": "", + "materials": "", + "description": "以按钮组的方式出现,常用于多项类似操作" + }, + { + "id": "09136cea", + "_RID": "row_6", + "name": "提示框", + "framework": "", + "materials": "", + "description": "Popover可通过对一个触发源操作触发弹出框,支持自定义弹出内容,延迟触发和渐变动画" + }, + { + "id": "a63b57d5", + "_RID": "row_7", + "name": "文字提示框", + "framework": "", + "materials": "", + "description": "动态显示提示信息,一般通过鼠标事件进行响应;提供 warning、error、info、success 四种类型显示不同类别的信" + }, + { + "id": "a0f6e8a3", + "_RID": "row_8", + "name": "树", + "framework": "", + "materials": "", + "description": "可进行展示有父子层级的数据,支持选择,异步加载等功能。但不推荐用它来展示菜单,展示菜单推荐使用树菜单" + }, + { + "id": "d1aa18fc", + "_RID": "row_9", + "name": "分页", + "framework": "", + "materials": "", + "description": "当数据量过多时,使用分页分解数据,常用于 Grid 和 Repeater 组件" + }, + { + "id": "ca49cc52", + "_RID": "row_10", + "name": "表格", + "framework": "", + "materials": "", + "description": "提供了非常强大数据表格功能,可以展示数据列表,可以对数据列表进行选择、编辑等" + }, + { + "id": "4e20ecc9", + "name": "搜索框", + "framework": "", + "materials": "", + "description": "指定条件对象进行搜索数据" + }, + { + "id": "6b093ee5", + "name": "折叠面板", + "framework": "", + "materials": "", + "description": "内容区可指定动态页面或自定义 html 等,支持展开收起操作" + }, + { + "id": "0a09abc0", + "name": "对话框", + "framework": "", + "materials": "", + "description": "模态对话框,在浮层中显示,引导用户进行相关操作" + }, + { + "id": "f814b901", + "name": "标签页签项", + "framework": "", + "materials": "", + "description": "tab页签" + }, + { + "id": "c5ae797c", + "name": "单选", + "framework": "", + "materials": "", + "description": "用于配置不同场景的选项,在一组备选项中进行单选" + }, + { + "id": "33d0c590", + "_RID": "row_13", + "name": "弹出编辑", + "framework": "", + "materials": "", + "description": "该组件只能在弹出的面板中选择数据,不能手动输入数据;弹出面板中显示为 Tree 组件或者 Grid 组件" + }, + { + "id": "16711dfa", + "_RID": "row_14", + "name": "下拉框", + "framework": "", + "materials": "", + "description": "Select 选择器是一种通过点击弹出下拉列表展示数据并进行选择的 UI 组件" + }, + { + "id": "a9fd190a", + "_RID": "row_15", + "name": "折叠面板项", + "framework": "", + "materials": "", + "description": "内容区可指定动态页面或自定义 html 等,支持展开收起操作" + }, + { + "id": "a7dfa9ec", + "_RID": "row_16", + "name": "复选框", + "framework": "", + "materials": "", + "description": "用于配置不同场景的选项,提供用户可在一组选项中进行多选" + }, + { + "id": "d4bb8330", + "name": "输入框", + "framework": "", + "materials": "", + "description": "通过鼠标或键盘输入字符" + }, + { + "id": "ced3dc83", + "name": "时间线", + "framework": "", + "materials": "", + "description": "时间线" + } + ], + "type": "array", + "columns": [ + { + "name": "name", + "type": "string", + "field": "name", + "title": "name", + "format": { + "max": 0, + "min": 0, + "dateTime": false, + "required": false, + "stringType": "" + } + }, + { + "name": "description", + "type": "string", + "field": "description", + "title": "description", + "format": { + "max": 0, + "min": 0, + "dateTime": false, + "required": false, + "stringType": "" + } + }, + { + "name": "isSelected", + "type": "string", + "field": "isSelected", + "title": "isSelected", + "format": { + "max": 0, + "min": 0, + "dateTime": false, + "required": false, + "stringType": "" + } + } + ], + "options": { + "uri": "http://localhost:9090/assets/json/bundle.json", + "method": "GET" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option \n}" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(data) { \n return data \n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {}" + } + }, + "tpl": null, + "app": "1", + "desc": null, + "created_at": "2022-07-04T02:20:07.000Z", + "updated_at": "2022-07-04T06:25:29.000Z" + }, + { + "id": 151, + "name": "selectedComponents", + "data": { + "columns": [ + { + "name": "name", + "title": "name", + "field": "name", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 0, + "dateTime": false + } + }, + { + "name": "description", + "title": "description", + "field": "description", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 0, + "dateTime": false + } + }, + { + "name": "isSelected", + "title": "isSelected", + "field": "isSelected", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 0, + "dateTime": false + } + } + ], + "type": "array", + "data": [ + { + "name": "标签页", + "description": "分隔内容上有关联但属于不同类别的数据集合", + "isSelected": "true", + "_RID": "row_2" + }, + { + "name": "布局列", + "description": "列配置信息", + "isSelected": "true", + "id": "76a7080a", + "_RID": "row_4" + }, + { + "name": "日期选择器", + "description": "用于设置/选择日期,包括年月/年月日/年月日时分/年月日时分秒日期格式", + "isSelected": "true", + "id": "76b20d73", + "_RID": "row_1" + }, + { + "name": "走马灯", + "description": "常用于一组图片或卡片轮播,当内容空间不足时,可以用走马灯的形式进行收纳,进行轮播展现", + "isSelected": "true", + "id": "4c884c3d" + } + ] + }, + "tpl": null, + "app": "1", + "desc": null, + "created_at": "2022-07-04T03:04:05.000Z", + "updated_at": "2022-07-04T03:43:40.000Z" + } + ], + "dataHandler": { + "type": "JSFunction", + "value": "function dataHanlder(res){\n return res;\n}" + } + }, + "i18n": { + "zh_CN": { + "lowcode_cca8d0ea": "应用", + "lowcode_c257d5e8": "查询", + "lowcode_61c8ac8c": "地方", + "lowcode_f53187a0": "测试", + "lowcode_97ad00dd": "创建物料资产包", + "lowcode_61dcef52": "terterere", + "lowcode_45f4c42a": "gdfgdf", + "lowcode_c6f5a652": "fsdaf", + "lowcode_34923432": "fdsafdsa", + "lowcode_48521e45": "fdsfds", + "lowcode_6534943e": "fdsafds", + "lowcode_44252642": "fdsafds", + "lowcode_2a743651": "sda", + "lowcode_24315357": "fdsafds", + "lowcode_44621691": "fdsafsd", + "lowcode_65636226": "fdsaf", + "lowcode_6426a4e2": "sd", + "lowcode_e41c6636": "aa", + "lowcode_51c23164": "aa", + "lowcode_17245b46": "aa", + "lowcode_4573143c": "aa", + "lowcode_56432442": "aa", + "lowcode_33566643": "aa", + "lowcode_565128f3": "aa", + "lowcode_56643835": "aa", + "lowcode_33311134": "aa", + "lowcode_44326643": "aa", + "lowcode_36223242": "aa" + }, + "en_US": { + "lowcode_cca8d0ea": "app", + "lowcode_c257d5e8": "search", + "lowcode_61c8ac8c": "dsdsa", + "lowcode_f53187a0": "test", + "lowcode_97ad00dd": "createMaterial", + "lowcode_61dcef52": "sadasda", + "lowcode_45f4c42a": "gfdgfd", + "lowcode_c6f5a652": "fsdafds", + "lowcode_34923432": "fdsafds", + "lowcode_6534943e": "fdsafdsa", + "lowcode_44252642": "aaaa", + "lowcode_2a743651": "fdsaf", + "lowcode_24315357": "fsdafds", + "lowcode_44621691": "sd", + "lowcode_65636226": "fdsfsd", + "lowcode_6426a4e2": "fdsafsd", + "lowcode_e41c6636": "aa", + "lowcode_51c23164": "aa", + "lowcode_17245b46": "aa", + "lowcode_4573143c": "a", + "lowcode_56432442": "aa", + "lowcode_33566643": "aa", + "lowcode_565128f3": "aa", + "lowcode_56643835": "aa", + "lowcode_33311134": "aa", + "lowcode_44326643": "aa", + "lowcode_36223242": "aa" + } + }, + "componentsTree": [], + "componentsMap": [ + { + "componentName": "TinyCarouselItem", + "package": "@opentiny/vue", + "exportName": "CarouselItem", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyCheckboxButton", + "package": "@opentiny/vue", + "exportName": "CheckboxButton", + "destructuring": true, + "version": "0.1.17" + }, + { + "componentName": "TinyTree", + "package": "@opentiny/vue", + "exportName": "Tree", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyPopover", + "package": "@opentiny/vue", + "exportName": "Popover", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyTooltip", + "package": "@opentiny/vue", + "exportName": "Tooltip", + "destructuring": true, + "version": "3.2.0" + }, + { + "componentName": "TinyCol", + "package": "@opentiny/vue", + "exportName": "Col", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyDropdownItem", + "package": "@opentiny/vue", + "exportName": "DropdownItem", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyPager", + "package": "@opentiny/vue", + "exportName": "Pager", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyPlusAccessdeclined", + "package": "@opentiny/vue", + "exportName": "AccessDeclined", + "destructuring": true, + "version": "3.4.1" + }, + { + "componentName": "TinyPlusFrozenPage", + "package": "@opentiny/vue", + "exportName": "FrozenPage", + "destructuring": true, + "version": "3.4.1" + }, + { + "componentName": "TinyPlusNonSupportRegion", + "package": "@opentiny/vue", + "exportName": "NonSupportRegion", + "destructuring": true, + "version": "3.4.1" + }, + { + "componentName": "TinyPlusBeta", + "package": "@opentiny/vue", + "exportName": "Beta", + "destructuring": true, + "version": "3.4.1" + }, + { + "componentName": "TinySearch", + "package": "@opentiny/vue", + "exportName": "Search", + "destructuring": true, + "version": "0.1.13" + }, + { + "componentName": "TinyRow", + "package": "@opentiny/vue", + "exportName": "Row", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyFormItem", + "package": "@opentiny/vue", + "exportName": "FormItem", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyAlert", + "package": "@opentiny/vue", + "exportName": "Alert", + "destructuring": true, + "version": "3.2.0" + }, + { + "componentName": "TinyInput", + "package": "@opentiny/vue", + "exportName": "Input", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyTabs", + "package": "@opentiny/vue", + "exportName": "Tabs", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyDropdownMenu", + "package": "@opentiny/vue", + "exportName": "DropdownMenu", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyDialogBox", + "package": "@opentiny/vue", + "exportName": "DialogBox", + "destructuring": true, + "version": "3.2.0" + }, + { + "componentName": "TinySwitch", + "package": "@opentiny/vue", + "exportName": "Switch", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyTimeLine", + "package": "@opentiny/vue", + "exportName": "TimeLine", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyTabItem", + "package": "@opentiny/vue", + "exportName": "TabItem", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyRadio", + "package": "@opentiny/vue", + "exportName": "Radio", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyForm", + "package": "@opentiny/vue", + "exportName": "Form", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyGrid", + "package": "@opentiny/vue", + "exportName": "Grid", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyGridColumn", + "package": "@opentiny/vue", + "exportName": "TinyGridColumn", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyNumeric", + "package": "@opentiny/vue", + "exportName": "Numeric", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyCheckboxGroup", + "package": "@opentiny/vue", + "exportName": "CheckboxGroup", + "destructuring": true, + "version": "0.1.17" + }, + { + "componentName": "TinyCheckbox", + "package": "@opentiny/vue", + "exportName": "Checkbox", + "destructuring": true, + "version": "3.20.0" + }, + { + "componentName": "TinySelect", + "package": "@opentiny/vue", + "exportName": "Select", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyButton", + "package": "@opentiny/vue", + "exportName": "Button", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyButtonGroup", + "package": "@opentiny/vue", + "exportName": "ButtonGroup", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyCarousel", + "package": "@opentiny/vue", + "exportName": "Carousel", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyPopeditor", + "package": "@opentiny/vue", + "exportName": "Popeditor", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyDatePicker", + "package": "@opentiny/vue", + "exportName": "DatePicker", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyDropdown", + "package": "@opentiny/vue", + "exportName": "Dropdown", + "destructuring": true, + "version": "0.1.20" + }, + { + "componentName": "TinyChartHistogram", + "package": "@opentiny/vue", + "exportName": "ChartHistogram", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyCollapse", + "package": "@opentiny/vue", + "exportName": "Collapse", + "destructuring": true, + "version": "3.20.0" + }, + { + "componentName": "TinyCollapseItem", + "package": "@opentiny/vue", + "exportName": "CollapseItem", + "destructuring": true, + "version": "3.20.0" + }, + { + "componentName": "TinyBreadcrumb", + "package": "@opentiny/vue", + "exportName": "Breadcrumb", + "destructuring": true, + "version": "3.20.0" + }, + { + "componentName": "TinyBreadcrumbItem", + "package": "@opentiny/vue", + "exportName": "BreadcrumbItem", + "destructuring": true, + "version": "3.20.0" + }, + { + "componentName": "TinyRate", + "package": "@opentiny/vue", + "exportName": "TinyRate", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinySlider", + "package": "@opentiny/vue", + "exportName": "TinySlider", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyCascader", + "package": "@opentiny/vue", + "exportName": "TinyCascader", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinySteps", + "package": "@opentiny/vue", + "exportName": "TinySteps", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyTreeMenu", + "package": "@opentiny/vue", + "exportName": "TinyTreeMenu", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyRadioGroup", + "package": "@opentiny/vue", + "exportName": "TinyRadioGroup", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "ElInput", + "package": "element-plus", + "exportName": "ElInput", + "destructuring": true, + "version": "2.4.2" + }, + { + "componentName": "ElButton", + "package": "element-plus", + "exportName": "ElButton", + "destructuring": true, + "version": "2.4.2" + }, + { + "componentName": "ElForm", + "package": "element-plus", + "exportName": "ElForm", + "destructuring": true, + "version": "2.4.2" + }, + { + "componentName": "ElFormItem", + "package": "element-plus", + "exportName": "ElFormItem", + "destructuring": true, + "version": "2.4.2" + }, + { + "componentName": "ElTable", + "package": "element-plus", + "exportName": "ElTable", + "destructuring": true, + "version": "2.4.2" + }, + { + "componentName": "ElTableColumn", + "package": "element-plus", + "exportName": "ElTableColumn", + "destructuring": true, + "version": "2.4.2" + }, + { + "componentName": "TinyProgress", + "package": "@opentiny/vue", + "exportName": "TinyProgress", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinySkeleton", + "package": "@opentiny/vue", + "exportName": "TinySkeleton", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyCard", + "package": "@opentiny/vue", + "exportName": "TinyCard", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyCalendar", + "package": "@opentiny/vue", + "exportName": "TinyCalendar", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyBadge", + "package": "@opentiny/vue", + "exportName": "TinyBadge", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyTag", + "package": "@opentiny/vue", + "exportName": "TinyTag", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyStatistic", + "package": "@opentiny/vue", + "exportName": "TinyStatistic", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsFunnel", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsFunnel", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsScatter", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsScatter", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsWaterfall", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsWaterfall", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsLine", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsLine", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsHistogram", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsHistogram", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsPie", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsPie", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsBar", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsBar", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsRing", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsRing", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsRadar", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsRadar", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "PortalHome", + "main": "common/components/home", + "destructuring": false, + "version": "1.0.0" + }, + { + "componentName": "PreviewBlock1", + "main": "preview", + "destructuring": false, + "version": "1.0.0" + }, + { + "componentName": "PortalHeader", + "main": "common", + "destructuring": false, + "version": "1.0.0" + }, + { + "componentName": "PortalBlock", + "main": "portal", + "destructuring": false, + "version": "1.0.0" + }, + { + "componentName": "PortalPermissionBlock", + "main": "", + "destructuring": false, + "version": "1.0.0" + } + ], + "bridge": [], + "utils": [ + { + "name": "axios", + "type": "npm", + "content": { + "type": "JSFunction", + "value": "", + "package": "axios", + "destructuring": false, + "exportName": "axios", + "cdnLink": "https://registry.npmmirror.com/axios/1.7.9/files/dist/esm/axios.min.js" + } + }, + { + "name": "Button", + "type": "npm", + "content": { + "package": "@opentiny/vue", + "version": "", + "exportName": "Button", + "subName": "", + "destructuring": true, + "main": "" + } + }, + { + "name": "Menu", + "type": "npm", + "content": { + "type": "JSFunction", + "value": "", + "package": "@opentiny/vue", + "exportName": "NavMenu", + "destructuring": true + } + }, + { + "name": "Modal ", + "type": "npm", + "content": { + "package": "@opentiny/vue", + "version": "", + "exportName": "Modal ", + "subName": "", + "destructuring": true, + "main": "" + } + }, + { + "name": "Pager", + "type": "npm", + "content": { + "package": "@opentiny/vue", + "version": "", + "exportName": "Pager", + "subName": "", + "destructuring": true, + "main": "" + } + }, + { + "name": "test", + "type": "function", + "content": { + "type": "JSFunction", + "value": "function test() {\r\n return 'test'\r\n}" + } + }, + { + "name": "util", + "type": "function", + "content": { + "type": "JSFunction", + "value": "function util () {\r\n console.log(321)\r\n}" + } + } + ], + "config": { + "sdkVersion": "1.0.3", + "historyMode": "hash", + "targetRootID": "app" + }, + "constants": "", + "css": "", + "version": "", + "_id": "40pUIT4ZLdwjOIF3" +} \ No newline at end of file diff --git a/mockServer/data/appsSchema/16.json b/mockServer/data/appsSchema/16.json new file mode 100644 index 0000000000..26431ede25 --- /dev/null +++ b/mockServer/data/appsSchema/16.json @@ -0,0 +1,2336 @@ +{ + "id": 16, + "meta": { + "name": "dashboard", + "tenant": 1, + "git_group": "", + "project_name": "", + "description": "数据看板", + "branch": "develop", + "is_demo": null, + "global_state": [], + "appId": "16", + "creator": "", + "gmt_create": "2022-06-08 03:19:01", + "gmt_modified": "2023-08-23 10:22:28" + }, + "dataSource": { + "list": [ + { + "id": 132, + "name": "getAllComponent", + "data": { + "data": [], + "type": "array" + }, + "tpl": null, + "app": "1", + "desc": null, + "created_at": "2022-06-28T06:26:26.000Z", + "updated_at": "2022-06-28T07:02:30.000Z" + }, + { + "id": 133, + "name": "getAllList", + "data": { + "columns": [ + { + "name": "test", + "title": "测试", + "field": "test", + "type": "string", + "format": {} + }, + { + "name": "test1", + "title": "测试1", + "field": "test1", + "type": "string", + "format": {} + } + ], + "type": "array", + "data": [ + { + "test": "test1", + "test1": "test1", + "_id": "341efc48" + }, + { + "test": "test2", + "test1": "test1", + "_id": "b86b516c" + }, + { + "test": "test3", + "test1": "test1", + "_id": "f680cd78" + } + ], + "options": { + "uri": "", + "method": "GET" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(data) { \n return data \n}" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option \n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {}" + } + }, + "tpl": null, + "app": "1", + "desc": null, + "created_at": "2022-06-28T07:32:16.000Z", + "updated_at": "2023-01-19T03:29:11.000Z" + }, + { + "id": 135, + "name": "getAllMaterialList", + "data": { + "columns": [ + { + "name": "id", + "title": "id", + "field": "id", + "type": "string", + "format": {} + }, + { + "name": "name", + "title": "name", + "field": "name", + "type": "string", + "format": {} + }, + { + "name": "framework", + "title": "framework", + "field": "framework", + "type": "string", + "format": { + "required": true + } + }, + { + "name": "components", + "title": "components", + "field": "components", + "type": "string", + "format": {} + }, + { + "name": "content", + "title": "content", + "field": "content", + "type": "string", + "format": {} + }, + { + "name": "url", + "title": "url", + "field": "url", + "type": "string", + "format": {} + }, + { + "name": "published_at", + "title": "published_at", + "field": "published_at", + "type": "string", + "format": {} + }, + { + "name": "created_at", + "title": "created_at", + "field": "created_at", + "type": "string", + "format": {} + }, + { + "name": "updated_at", + "title": "updated_at", + "field": "updated_at", + "type": "string", + "format": {} + }, + { + "name": "published", + "title": "published", + "field": "published", + "type": "string", + "format": {} + }, + { + "name": "last_build_info", + "title": "last_build_info", + "field": "last_build_info", + "type": "string", + "format": {} + }, + { + "name": "tenant", + "title": "tenant", + "field": "tenant", + "type": "string", + "format": {} + }, + { + "name": "version", + "title": "version", + "field": "version", + "type": "string", + "format": {} + }, + { + "name": "description", + "title": "description", + "field": "description", + "type": "string", + "format": {} + } + ], + "type": "array", + "data": [ + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "2a23e653" + }, + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "06b253be" + }, + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "c55a41ed" + }, + { + "id": "f37123ec", + "url": "", + "name": "ng-material", + "tenant": "", + "content": "", + "version": "1.0.0", + "framework": "Angular", + "published": "", + "components": "", + "created_at": "2021-11-02T11:32:22.000Z", + "updated_at": "2021-11-02T11:32:22.000Z", + "description": "angular组件库物料", + "published_at": "2021-11-02T11:32:22.000Z", + "last_build_info": "", + "_id": "f37123ec" + }, + { + "id": "7a63c1a2", + "url": "", + "name": "tiny-vue", + "tenant": "", + "content": "Tiny Vue物料", + "version": "1.0.0", + "framework": "Vue", + "published": "", + "components": "", + "created_at": "", + "updated_at": "", + "description": "Tiny Vue物料", + "published_at": "", + "last_build_info": "", + "_id": "7a63c1a2" + } + ], + "options": { + "uri": "", + "method": "GET" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option \n}" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(data) { \n return data \n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {}" + } + }, + "tpl": null, + "app": "1", + "desc": null, + "created_at": "2022-06-29T00:57:50.000Z", + "updated_at": "2023-05-15T02:37:12.000Z" + }, + { + "id": 139, + "name": "treedata", + "data": { + "data": [ + { + "label": "level111", + "value": "111", + "id": "f6609643", + "pid": "", + "_RID": "row_4" + }, + { + "label": "level1-son", + "value": "111-1", + "id": "af1f937f", + "pid": "f6609643", + "_RID": "row_5" + }, + { + "label": "level222", + "value": "222", + "id": "28e3709c", + "pid": "", + "_RID": "row_6" + }, + { + "label": "level2-son", + "value": "222-1", + "id": "6b571bef", + "pid": "28e3709c", + "_RID": "row_5" + }, + { + "id": "6317c2cc", + "pid": "fdfa", + "label": "fsdfaa", + "value": "fsadf", + "_RID": "row_6" + }, + { + "id": "9cce369f", + "pid": "test", + "label": "test1", + "value": "001" + } + ], + "type": "tree" + }, + "tpl": null, + "app": "1", + "desc": null, + "created_at": "2022-06-30T06:13:57.000Z", + "updated_at": "2022-07-29T03:14:55.000Z" + }, + { + "id": 150, + "name": "componentList", + "data": { + "data": [ + { + "_RID": "row_1", + "name": "表单", + "isSelected": "true", + "description": "由按钮、输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据" + }, + { + "name": "按钮", + "isSelected": "false", + "description": "常用的操作按钮,提供包括默认按钮、图标按钮、图片按钮、下拉按钮等类型" + }, + { + "id": "490f8a00", + "_RID": "row_3", + "name": "表单项", + "framework": "", + "materials": "", + "description": "Form 组件下的 FormItem 配置" + }, + { + "id": "c259b8b3", + "_RID": "row_4", + "name": "开关", + "framework": "", + "materials": "", + "description": "关闭或打开" + }, + { + "id": "083ed9c7", + "_RID": "row_5", + "name": "互斥按钮组", + "framework": "", + "materials": "", + "description": "以按钮组的方式出现,常用于多项类似操作" + }, + { + "id": "09136cea", + "_RID": "row_6", + "name": "提示框", + "framework": "", + "materials": "", + "description": "Popover可通过对一个触发源操作触发弹出框,支持自定义弹出内容,延迟触发和渐变动画" + }, + { + "id": "a63b57d5", + "_RID": "row_7", + "name": "文字提示框", + "framework": "", + "materials": "", + "description": "动态显示提示信息,一般通过鼠标事件进行响应;提供 warning、error、info、success 四种类型显示不同类别的信" + }, + { + "id": "a0f6e8a3", + "_RID": "row_8", + "name": "树", + "framework": "", + "materials": "", + "description": "可进行展示有父子层级的数据,支持选择,异步加载等功能。但不推荐用它来展示菜单,展示菜单推荐使用树菜单" + }, + { + "id": "d1aa18fc", + "_RID": "row_9", + "name": "分页", + "framework": "", + "materials": "", + "description": "当数据量过多时,使用分页分解数据,常用于 Grid 和 Repeater 组件" + }, + { + "id": "ca49cc52", + "_RID": "row_10", + "name": "表格", + "framework": "", + "materials": "", + "description": "提供了非常强大数据表格功能,可以展示数据列表,可以对数据列表进行选择、编辑等" + }, + { + "id": "4e20ecc9", + "name": "搜索框", + "framework": "", + "materials": "", + "description": "指定条件对象进行搜索数据" + }, + { + "id": "6b093ee5", + "name": "折叠面板", + "framework": "", + "materials": "", + "description": "内容区可指定动态页面或自定义 html 等,支持展开收起操作" + }, + { + "id": "0a09abc0", + "name": "对话框", + "framework": "", + "materials": "", + "description": "模态对话框,在浮层中显示,引导用户进行相关操作" + }, + { + "id": "f814b901", + "name": "标签页签项", + "framework": "", + "materials": "", + "description": "tab页签" + }, + { + "id": "c5ae797c", + "name": "单选", + "framework": "", + "materials": "", + "description": "用于配置不同场景的选项,在一组备选项中进行单选" + }, + { + "id": "33d0c590", + "_RID": "row_13", + "name": "弹出编辑", + "framework": "", + "materials": "", + "description": "该组件只能在弹出的面板中选择数据,不能手动输入数据;弹出面板中显示为 Tree 组件或者 Grid 组件" + }, + { + "id": "16711dfa", + "_RID": "row_14", + "name": "下拉框", + "framework": "", + "materials": "", + "description": "Select 选择器是一种通过点击弹出下拉列表展示数据并进行选择的 UI 组件" + }, + { + "id": "a9fd190a", + "_RID": "row_15", + "name": "折叠面板项", + "framework": "", + "materials": "", + "description": "内容区可指定动态页面或自定义 html 等,支持展开收起操作" + }, + { + "id": "a7dfa9ec", + "_RID": "row_16", + "name": "复选框", + "framework": "", + "materials": "", + "description": "用于配置不同场景的选项,提供用户可在一组选项中进行多选" + }, + { + "id": "d4bb8330", + "name": "输入框", + "framework": "", + "materials": "", + "description": "通过鼠标或键盘输入字符" + }, + { + "id": "ced3dc83", + "name": "时间线", + "framework": "", + "materials": "", + "description": "时间线" + } + ], + "type": "array", + "columns": [ + { + "name": "name", + "type": "string", + "field": "name", + "title": "name", + "format": { + "max": 0, + "min": 0, + "dateTime": false, + "required": false, + "stringType": "" + } + }, + { + "name": "description", + "type": "string", + "field": "description", + "title": "description", + "format": { + "max": 0, + "min": 0, + "dateTime": false, + "required": false, + "stringType": "" + } + }, + { + "name": "isSelected", + "type": "string", + "field": "isSelected", + "title": "isSelected", + "format": { + "max": 0, + "min": 0, + "dateTime": false, + "required": false, + "stringType": "" + } + } + ], + "options": { + "uri": "http://localhost:9090/assets/json/bundle.json", + "method": "GET" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option \n}" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(data) { \n return data \n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {}" + } + }, + "tpl": null, + "app": "1", + "desc": null, + "created_at": "2022-07-04T02:20:07.000Z", + "updated_at": "2022-07-04T06:25:29.000Z" + }, + { + "id": 151, + "name": "selectedComponents", + "data": { + "columns": [ + { + "name": "name", + "title": "name", + "field": "name", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 0, + "dateTime": false + } + }, + { + "name": "description", + "title": "description", + "field": "description", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 0, + "dateTime": false + } + }, + { + "name": "isSelected", + "title": "isSelected", + "field": "isSelected", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 0, + "dateTime": false + } + } + ], + "type": "array", + "data": [ + { + "name": "标签页", + "description": "分隔内容上有关联但属于不同类别的数据集合", + "isSelected": "true", + "_RID": "row_2" + }, + { + "name": "布局列", + "description": "列配置信息", + "isSelected": "true", + "id": "76a7080a", + "_RID": "row_4" + }, + { + "name": "日期选择器", + "description": "用于设置/选择日期,包括年月/年月日/年月日时分/年月日时分秒日期格式", + "isSelected": "true", + "id": "76b20d73", + "_RID": "row_1" + }, + { + "name": "走马灯", + "description": "常用于一组图片或卡片轮播,当内容空间不足时,可以用走马灯的形式进行收纳,进行轮播展现", + "isSelected": "true", + "id": "4c884c3d" + } + ] + }, + "tpl": null, + "app": "1", + "desc": null, + "created_at": "2022-07-04T03:04:05.000Z", + "updated_at": "2022-07-04T03:43:40.000Z" + } + ], + "dataHandler": { + "type": "JSFunction", + "value": "function dataHanlder(res){\n return res;\n}" + } + }, + "i18n": { + "zh_CN": { + "lowcode_cca8d0ea": "应用", + "lowcode_c257d5e8": "查询", + "lowcode_61c8ac8c": "地方", + "lowcode_f53187a0": "测试", + "lowcode_97ad00dd": "创建物料资产包", + "lowcode_61dcef52": "terterere", + "lowcode_45f4c42a": "gdfgdf", + "lowcode_c6f5a652": "fsdaf", + "lowcode_34923432": "fdsafdsa", + "lowcode_48521e45": "fdsfds", + "lowcode_6534943e": "fdsafds", + "lowcode_44252642": "fdsafds", + "lowcode_2a743651": "sda", + "lowcode_24315357": "fdsafds", + "lowcode_44621691": "fdsafsd", + "lowcode_65636226": "fdsaf", + "lowcode_6426a4e2": "sd", + "lowcode_e41c6636": "aa", + "lowcode_51c23164": "aa", + "lowcode_17245b46": "aa", + "lowcode_4573143c": "aa", + "lowcode_56432442": "aa", + "lowcode_33566643": "aa", + "lowcode_565128f3": "aa", + "lowcode_56643835": "aa", + "lowcode_33311134": "aa", + "lowcode_44326643": "aa", + "lowcode_36223242": "aa" + }, + "en_US": { + "lowcode_cca8d0ea": "app", + "lowcode_c257d5e8": "search", + "lowcode_61c8ac8c": "dsdsa", + "lowcode_f53187a0": "test", + "lowcode_97ad00dd": "createMaterial", + "lowcode_61dcef52": "sadasda", + "lowcode_45f4c42a": "gfdgfd", + "lowcode_c6f5a652": "fsdafds", + "lowcode_34923432": "fdsafds", + "lowcode_6534943e": "fdsafdsa", + "lowcode_44252642": "aaaa", + "lowcode_2a743651": "fdsaf", + "lowcode_24315357": "fsdafds", + "lowcode_44621691": "sd", + "lowcode_65636226": "fdsfsd", + "lowcode_6426a4e2": "fdsafsd", + "lowcode_e41c6636": "aa", + "lowcode_51c23164": "aa", + "lowcode_17245b46": "aa", + "lowcode_4573143c": "a", + "lowcode_56432442": "aa", + "lowcode_33566643": "aa", + "lowcode_565128f3": "aa", + "lowcode_56643835": "aa", + "lowcode_33311134": "aa", + "lowcode_44326643": "aa", + "lowcode_36223242": "aa" + } + }, + "componentsTree": [ + { + "state": { + "dataDisk": [ + 1, + 2, + 3 + ] + }, + "methods": {}, + "componentName": "Page", + "css": "body {\r\n background-color:#eef0f5 ;\r\n margin-bottom: 80px;\r\n}", + "props": {}, + "children": [ + { + "componentName": "div", + "props": { + "style": "padding-bottom: 10px; padding-top: 10px;" + }, + "id": "2b2cabf0", + "children": [ + { + "componentName": "TinyTimeLine", + "props": { + "active": 2, + "data": [ + { + "name": "基础配置" + }, + { + "name": "网络配置" + }, + { + "name": "高级配置" + }, + { + "name": "确认配置" + } + ], + "horizontal": true, + "style": "border-radius: 0px;" + }, + "id": "dd764b17" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "id": "30c94cc8", + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "计费模式" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "包年/包月", + "value": "1" + }, + { + "text": "按需计费", + "value": "2" + } + ], + "modelValue": "1" + }, + "id": "a8d84361" + } + ], + "id": "9f39f3e7" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "区域" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "乌兰察布二零一", + "value": "1" + } + ], + "modelValue": "1", + "style": "border-radius: 0px; margin-right: 10px;" + }, + "id": "c97ccd99" + }, + { + "componentName": "Text", + "props": { + "text": "温馨提示:页面左上角切换区域", + "style": "background-color: #f5f5f5; color: #8a8e99; font-size: 12px;" + }, + "id": "20923497" + }, + { + "componentName": "Text", + "props": { + "text": "不同区域的云服务产品之间内网互不相通;请就近选择靠近您业务的区域,可减少网络时延,提高访问速度", + "style": "display: block; color: #8a8e99; border-radius: 0px; font-size: 12px;" + }, + "id": "54780a26" + } + ], + "id": "4966384d" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "可用区", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "可用区1", + "value": "1" + }, + { + "text": "可用区2", + "value": "2" + }, + { + "text": "可用区3", + "value": "3" + } + ], + "modelValue": "1" + }, + "id": "6184481b" + } + ], + "id": "690837bf" + } + ], + "id": "b6a425d4" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "CPU架构" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "x86计算", + "value": "1" + }, + { + "text": "鲲鹏计算", + "value": "2" + } + ], + "modelValue": "1" + }, + "id": "7d33ced7" + } + ], + "id": "05ed5a79" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "区域" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex; justify-content: flex-start; align-items: center;" + }, + "id": "606edf78", + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center; margin-right: 10px;" + }, + "id": "f3f98246", + "children": [ + { + "componentName": "Text", + "props": { + "text": "vCPUs", + "style": "width: 80px;" + }, + "id": "c287437e" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "id": "4c43286b" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center; margin-right: 10px;" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "内存", + "style": "width: 80px; border-radius: 0px;" + }, + "id": "38b8fa1f" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "id": "cd33328e" + } + ], + "id": "2b2c678f" + }, + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center;" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "规格名称", + "style": "width: 80px;" + }, + "id": "d3eb6352" + }, + { + "componentName": "TinySearch", + "props": { + "modelValue": "", + "placeholder": "输入关键词" + }, + "id": "21cb9282" + } + ], + "id": "b8e0f35c" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "border-radius: 0px;" + }, + "id": "5000c83e", + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "通用计算型", + "value": "1" + }, + { + "text": "通用计算增强型", + "value": "2" + }, + { + "text": "内存优化型", + "value": "3" + }, + { + "text": "内存优化型", + "value": "4" + }, + { + "text": "磁盘增强型", + "value": "5" + }, + { + "text": "超高I/O型", + "value": "6" + }, + { + "text": "GPU加速型", + "value": "7" + } + ], + "modelValue": "1", + "style": "border-radius: 0px; margin-top: 12px;" + }, + "id": "b8724703" + }, + { + "componentName": "TinyGrid", + "props": { + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "columns": [ + { + "type": "radio", + "width": 60 + }, + { + "field": "employees", + "title": "规格名称" + }, + { + "field": "created_date", + "title": "vCPUs | 内存(GiB)", + "sortable": true + }, + { + "field": "city", + "title": "CPU", + "sortable": true + }, + { + "title": "基准 / 最大带宽\t", + "sortable": true + }, + { + "title": "内网收发包", + "sortable": true + } + ], + "data": [ + { + "id": "1", + "name": "GFD科技有限公司", + "city": "福州", + "employees": 800, + "created_date": "2014-04-30 00:56:00", + "boole": false + }, + { + "id": "2", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true + } + ], + "style": "margin-top: 12px; border-radius: 0px;", + "auto-resize": true + }, + "id": "77701c25" + }, + { + "componentName": "div", + "props": { + "style": "margin-top: 12px; border-radius: 0px;" + }, + "id": "3339838b", + "children": [ + { + "componentName": "Text", + "props": { + "text": "当前规格", + "style": "width: 150px; display: inline-block;" + }, + "id": "203b012b" + }, + { + "componentName": "Text", + "props": { + "text": "通用计算型 | Si2.large.2 | 2vCPUs | 4 GiB", + "style": "font-weight: 700;" + }, + "id": "87723f52" + } + ] + } + ] + } + ], + "id": "657fb2fc" + } + ], + "id": "d19b15cf" + } + ], + "id": "9991228b" + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "镜像", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "公共镜像", + "value": "1" + }, + { + "text": "私有镜像", + "value": "2" + }, + { + "text": "共享镜像", + "value": "3" + } + ], + "modelValue": "1" + }, + "id": "922b14cb" + }, + { + "componentName": "div", + "props": { + "style": "display: flex; margin-top: 12px; border-radius: 0px;" + }, + "id": "6b679524", + "children": [ + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 170px; margin-right: 10px;" + }, + "id": "4851fff7" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 340px;" + }, + "id": "a7183eb7" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "margin-top: 12px;" + }, + "id": "57aee314", + "children": [ + { + "componentName": "Text", + "props": { + "text": "请注意操作系统的语言类型。", + "style": "color: #e37d29;" + }, + "id": "56d36c27" + } + ] + } + ], + "id": "e3b02436" + } + ], + "id": "59aebf2b" + } + ], + "id": "87ff7b99" + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "系统盘", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex;" + }, + "id": "cddba5b8", + "children": [ + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 200px; margin-right: 10px;" + }, + "id": "a97fbe15" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px;" + }, + "id": "1cde4c0f" + }, + { + "componentName": "Text", + "props": { + "text": "GiB \nIOPS上限240,IOPS突发上限5,000", + "style": "color: #575d6c; font-size: 12px;" + }, + "id": "2815d82d" + } + ] + } + ], + "id": "50239a3a" + } + ], + "id": "e8582986" + }, + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "数据盘", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "margin-top: 12px; display: flex;" + }, + "id": "728c9825", + "children": [ + { + "componentName": "Icon", + "props": { + "style": "margin-right: 10px; width: 16px; height: 16px;", + "name": "IconPanelMini" + }, + "id": "fded6930" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 200px; margin-right: 10px;" + }, + "id": "62734e3f" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px;" + }, + "id": "667c7926" + }, + { + "componentName": "Text", + "props": { + "text": "GiB \nIOPS上限600,IOPS突发上限5,000", + "style": "color: #575d6c; font-size: 12px; margin-right: 10px;" + }, + "id": "e7bc36d6" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px;" + }, + "id": "1bd56dc0" + } + ], + "loop": { + "type": "JSExpression", + "value": "this.state.dataDisk" + } + }, + { + "componentName": "div", + "props": { + "style": "display: flex; margin-top: 12px; border-radius: 0px;" + }, + "children": [ + { + "componentName": "Icon", + "props": { + "name": "IconPlus", + "style": "width: 16px; height: 16px; margin-right: 10px;" + }, + "id": "65c89f2b" + }, + { + "componentName": "Text", + "props": { + "text": "增加一块数据盘", + "style": "font-size: 12px; border-radius: 0px; margin-right: 10px;" + }, + "id": "cb344071" + }, + { + "componentName": "Text", + "props": { + "text": "您还可以挂载 21 块磁盘(云硬盘)", + "style": "color: #8a8e99; font-size: 12px;" + }, + "id": "80eea996" + } + ], + "id": "e9e530ab" + } + ], + "id": "078e03ef" + } + ], + "id": "ccef886e" + } + ], + "id": "0fb7bd74" + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px;z-index:1;border-style: solid; border-color: #ffffff; padding-top: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; position: fixed; inset: auto 0% 0% 0%; height: 80px; line-height: 80px; border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [], + "id": "21ed4475" + }, + { + "componentName": "TinyRow", + "props": { + "style": "border-radius: 0px; height: 100%;" + }, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": 8 + }, + "id": "b9d051a5", + "children": [ + { + "componentName": "TinyRow", + "props": { + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": 5, + "style": "display: flex;" + }, + "id": "02352776", + "children": [ + { + "componentName": "Text", + "props": { + "text": "购买量", + "style": "margin-right: 10px;" + }, + "id": "0cd9ed5c" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px;" + }, + "id": "2f9cf442" + }, + { + "componentName": "Text", + "props": { + "text": "台" + }, + "id": "facd4481" + } + ] + }, + { + "componentName": "TinyCol", + "props": { + "span": 7 + }, + "id": "82b6c659", + "children": [ + { + "componentName": "div", + "props": {}, + "id": "9cd65874", + "children": [ + { + "componentName": "Text", + "props": { + "text": "配置费用", + "style": "font-size: 12px;" + }, + "id": "b5a0a0da" + }, + { + "componentName": "Text", + "props": { + "text": "¥1.5776", + "style": "padding-left: 10px; padding-right: 10px; color: #de504e;" + }, + "id": "d9464214" + }, + { + "componentName": "Text", + "props": { + "text": "/小时", + "style": "font-size: 12px;" + }, + "id": "af7cc5e6" + } + ] + }, + { + "componentName": "div", + "props": {}, + "id": "89063830", + "children": [ + { + "componentName": "Text", + "props": { + "text": "参考价格,具体扣费请以账单为准。", + "style": "font-size: 12px; border-radius: 0px;" + }, + "id": "d8995fbc" + }, + { + "componentName": "Text", + "props": { + "text": "了解计费详情", + "style": "font-size: 12px; color: #344899;" + }, + "id": "b383c3e2" + } + ] + } + ] + } + ], + "id": "94fc0e43" + } + ] + }, + { + "componentName": "TinyCol", + "props": { + "span": 4, + "style": "display: flex; flex-direction: row-reverse; border-radius: 0px; height: 100%; justify-content: flex-start; align-items: center;" + }, + "id": "10b73009", + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": "下一步: 网络配置", + "type": "danger", + "style": "max-width: unset;" + }, + "id": "0b584011" + } + ] + } + ], + "id": "d414a473" + } + ], + "id": "e8ec029b" + } + ], + "fileName": "createVm", + "meta": { + "id": "1", + "parentId": "0", + "group": "staticPages", + "occupier": { + "id": 86, + "username": "开发者", + "email": "demo@example.com", + "provider": null, + "password": null, + "confirmationToken": null, + "confirmed": true, + "blocked": null, + "role": null, + "created_by": null, + "updated_by": null, + "created_at": "2022-05-27T16:50:44.000Z", + "updated_at": "2022-05-27T16:50:44.000Z", + "block": null, + "is_admin": true, + "is_public": null + }, + "isHome": false, + "router": "createVm", + "rootElement": "div", + "creator": "", + "gmt_create": "2022-07-21 03:08:20", + "gmt_modified": "2022-07-21 05:18:26" + } + } + ], + "componentsMap": [ + { + "componentName": "TinyCarouselItem", + "package": "@opentiny/vue", + "exportName": "CarouselItem", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyCheckboxButton", + "package": "@opentiny/vue", + "exportName": "CheckboxButton", + "destructuring": true, + "version": "0.1.17" + }, + { + "componentName": "TinyTree", + "package": "@opentiny/vue", + "exportName": "Tree", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyPopover", + "package": "@opentiny/vue", + "exportName": "Popover", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyTooltip", + "package": "@opentiny/vue", + "exportName": "Tooltip", + "destructuring": true, + "version": "3.2.0" + }, + { + "componentName": "TinyCol", + "package": "@opentiny/vue", + "exportName": "Col", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyDropdownItem", + "package": "@opentiny/vue", + "exportName": "DropdownItem", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyPager", + "package": "@opentiny/vue", + "exportName": "Pager", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyPlusAccessdeclined", + "package": "@opentiny/vue", + "exportName": "AccessDeclined", + "destructuring": true, + "version": "3.4.1" + }, + { + "componentName": "TinyPlusFrozenPage", + "package": "@opentiny/vue", + "exportName": "FrozenPage", + "destructuring": true, + "version": "3.4.1" + }, + { + "componentName": "TinyPlusNonSupportRegion", + "package": "@opentiny/vue", + "exportName": "NonSupportRegion", + "destructuring": true, + "version": "3.4.1" + }, + { + "componentName": "TinyPlusBeta", + "package": "@opentiny/vue", + "exportName": "Beta", + "destructuring": true, + "version": "3.4.1" + }, + { + "componentName": "TinySearch", + "package": "@opentiny/vue", + "exportName": "Search", + "destructuring": true, + "version": "0.1.13" + }, + { + "componentName": "TinyRow", + "package": "@opentiny/vue", + "exportName": "Row", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyFormItem", + "package": "@opentiny/vue", + "exportName": "FormItem", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyAlert", + "package": "@opentiny/vue", + "exportName": "Alert", + "destructuring": true, + "version": "3.2.0" + }, + { + "componentName": "TinyInput", + "package": "@opentiny/vue", + "exportName": "Input", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyTabs", + "package": "@opentiny/vue", + "exportName": "Tabs", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyDropdownMenu", + "package": "@opentiny/vue", + "exportName": "DropdownMenu", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyDialogBox", + "package": "@opentiny/vue", + "exportName": "DialogBox", + "destructuring": true, + "version": "3.2.0" + }, + { + "componentName": "TinySwitch", + "package": "@opentiny/vue", + "exportName": "Switch", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyTimeLine", + "package": "@opentiny/vue", + "exportName": "TimeLine", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyTabItem", + "package": "@opentiny/vue", + "exportName": "TabItem", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyRadio", + "package": "@opentiny/vue", + "exportName": "Radio", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyForm", + "package": "@opentiny/vue", + "exportName": "Form", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyGrid", + "package": "@opentiny/vue", + "exportName": "Grid", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyGridColumn", + "package": "@opentiny/vue", + "exportName": "TinyGridColumn", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyNumeric", + "package": "@opentiny/vue", + "exportName": "Numeric", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyCheckboxGroup", + "package": "@opentiny/vue", + "exportName": "CheckboxGroup", + "destructuring": true, + "version": "0.1.17" + }, + { + "componentName": "TinyCheckbox", + "package": "@opentiny/vue", + "exportName": "Checkbox", + "destructuring": true, + "version": "3.20.0" + }, + { + "componentName": "TinySelect", + "package": "@opentiny/vue", + "exportName": "Select", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyButton", + "package": "@opentiny/vue", + "exportName": "Button", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyButtonGroup", + "package": "@opentiny/vue", + "exportName": "ButtonGroup", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyCarousel", + "package": "@opentiny/vue", + "exportName": "Carousel", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyPopeditor", + "package": "@opentiny/vue", + "exportName": "Popeditor", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyDatePicker", + "package": "@opentiny/vue", + "exportName": "DatePicker", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyDropdown", + "package": "@opentiny/vue", + "exportName": "Dropdown", + "destructuring": true, + "version": "0.1.20" + }, + { + "componentName": "TinyChartHistogram", + "package": "@opentiny/vue", + "exportName": "ChartHistogram", + "destructuring": true, + "version": "0.1.16" + }, + { + "componentName": "TinyCollapse", + "package": "@opentiny/vue", + "exportName": "Collapse", + "destructuring": true, + "version": "3.20.0" + }, + { + "componentName": "TinyCollapseItem", + "package": "@opentiny/vue", + "exportName": "CollapseItem", + "destructuring": true, + "version": "3.20.0" + }, + { + "componentName": "TinyBreadcrumb", + "package": "@opentiny/vue", + "exportName": "Breadcrumb", + "destructuring": true, + "version": "3.20.0" + }, + { + "componentName": "TinyBreadcrumbItem", + "package": "@opentiny/vue", + "exportName": "BreadcrumbItem", + "destructuring": true, + "version": "3.20.0" + }, + { + "componentName": "TinyRate", + "package": "@opentiny/vue", + "exportName": "TinyRate", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinySlider", + "package": "@opentiny/vue", + "exportName": "TinySlider", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyCascader", + "package": "@opentiny/vue", + "exportName": "TinyCascader", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinySteps", + "package": "@opentiny/vue", + "exportName": "TinySteps", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyTreeMenu", + "package": "@opentiny/vue", + "exportName": "TinyTreeMenu", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyRadioGroup", + "package": "@opentiny/vue", + "exportName": "TinyRadioGroup", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "ElInput", + "package": "element-plus", + "exportName": "ElInput", + "destructuring": true, + "version": "2.4.2" + }, + { + "componentName": "ElButton", + "package": "element-plus", + "exportName": "ElButton", + "destructuring": true, + "version": "2.4.2" + }, + { + "componentName": "ElForm", + "package": "element-plus", + "exportName": "ElForm", + "destructuring": true, + "version": "2.4.2" + }, + { + "componentName": "ElFormItem", + "package": "element-plus", + "exportName": "ElFormItem", + "destructuring": true, + "version": "2.4.2" + }, + { + "componentName": "ElTable", + "package": "element-plus", + "exportName": "ElTable", + "destructuring": true, + "version": "2.4.2" + }, + { + "componentName": "ElTableColumn", + "package": "element-plus", + "exportName": "ElTableColumn", + "destructuring": true, + "version": "2.4.2" + }, + { + "componentName": "TinyProgress", + "package": "@opentiny/vue", + "exportName": "TinyProgress", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinySkeleton", + "package": "@opentiny/vue", + "exportName": "TinySkeleton", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyCard", + "package": "@opentiny/vue", + "exportName": "TinyCard", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyCalendar", + "package": "@opentiny/vue", + "exportName": "TinyCalendar", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyBadge", + "package": "@opentiny/vue", + "exportName": "TinyBadge", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyTag", + "package": "@opentiny/vue", + "exportName": "TinyTag", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyStatistic", + "package": "@opentiny/vue", + "exportName": "TinyStatistic", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsFunnel", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsFunnel", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsScatter", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsScatter", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsWaterfall", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsWaterfall", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsLine", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsLine", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsHistogram", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsHistogram", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsPie", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsPie", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsBar", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsBar", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsRing", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsRing", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "TinyHuichartsRadar", + "package": "@opentiny/vue-huicharts", + "exportName": "TinyHuichartsRadar", + "destructuring": true, + "version": "3.22.0" + }, + { + "componentName": "PortalHome", + "main": "common/components/home", + "destructuring": false, + "version": "1.0.0" + }, + { + "componentName": "PreviewBlock1", + "main": "preview", + "destructuring": false, + "version": "1.0.0" + }, + { + "componentName": "PortalHeader", + "main": "common", + "destructuring": false, + "version": "1.0.0" + }, + { + "componentName": "PortalBlock", + "main": "portal", + "destructuring": false, + "version": "1.0.0" + }, + { + "componentName": "PortalPermissionBlock", + "main": "", + "destructuring": false, + "version": "1.0.0" + } + ], + "bridge": [], + "utils": [ + { + "name": "axios", + "type": "npm", + "content": { + "type": "JSFunction", + "value": "", + "package": "axios", + "destructuring": false, + "exportName": "axios", + "cdnLink": "https://registry.npmmirror.com/axios/1.7.9/files/dist/esm/axios.min.js" + } + }, + { + "name": "Button", + "type": "npm", + "content": { + "package": "@opentiny/vue", + "version": "", + "exportName": "Button", + "subName": "", + "destructuring": true, + "main": "" + } + }, + { + "name": "Menu", + "type": "npm", + "content": { + "type": "JSFunction", + "value": "", + "package": "@opentiny/vue", + "exportName": "NavMenu", + "destructuring": true + } + }, + { + "name": "Modal ", + "type": "npm", + "content": { + "package": "@opentiny/vue", + "version": "", + "exportName": "Modal ", + "subName": "", + "destructuring": true, + "main": "" + } + }, + { + "name": "Pager", + "type": "npm", + "content": { + "package": "@opentiny/vue", + "version": "", + "exportName": "Pager", + "subName": "", + "destructuring": true, + "main": "" + } + }, + { + "name": "test", + "type": "function", + "content": { + "type": "JSFunction", + "value": "function test() {\r\n return 'test'\r\n}" + } + }, + { + "name": "util", + "type": "function", + "content": { + "type": "JSFunction", + "value": "function util () {\r\n console.log(321)\r\n}" + } + } + ], + "config": { + "sdkVersion": "1.0.3", + "historyMode": "hash", + "targetRootID": "app" + }, + "constants": "", + "css": "", + "version": "", + "_id": "m8VpJ5x1w5Ajr72S" +} \ No newline at end of file diff --git "a/mockServer/data/blockCategories/\346\210\221\347\232\204\345\210\206\347\261\273.json" "b/mockServer/data/blockCategories/\346\210\221\347\232\204\345\210\206\347\261\273.json" new file mode 100644 index 0000000000..21dca5b466 --- /dev/null +++ "b/mockServer/data/blockCategories/\346\210\221\347\232\204\345\210\206\347\261\273.json" @@ -0,0 +1,86 @@ +{ + "id": "L0fyFYECrNiRZMiX", + "app": { + "id": 1, + "name": "portal-app", + "app_website": null, + "platform": { + "id": 897, + "name": "portal-platform" + }, + "obs_url": "", + "created_by": null, + "updated_by": null, + "created_at": "2022-06-08T07:19:01.000Z", + "updated_at": "2023-09-04T08:55:40.000Z", + "state": null, + "published": false, + "createdBy": 86, + "updatedBy": 564, + "tenant": 1, + "home_page": "1", + "css": null, + "config": {}, + "git_group": "", + "project_name": "", + "constants": null, + "data_handler": { + "type": "JSFunction", + "value": "function dataHanlder(res){\n return res;\n}" + }, + "description": "demo应用", + "latest": 22, + "platform_history": null, + "editor_url": "", + "branch": "develop", + "visit_url": null, + "is_demo": null, + "image_url": "", + "is_default": true, + "template_type": null, + "set_template_time": null, + "set_template_by": null, + "set_default_by": 169, + "framework": "Vue", + "global_state": [], + "default_lang": null, + "extend_config": { + "business": { + "serviceName": "", + "endpointName": "cce", + "endpointId": "ee", + "serviceId": "ee", + "router": "ee" + }, + "env": { + "alpha": { + "regions": [ + { + "name": "", + "baseUrl": "", + "isDefault": false + } + ], + "isDefault": true + } + }, + "type": "console" + }, + "assets_url": "", + "data_hash": "ae128e37f6bc378f1b9c21d75bd05551", + "can_associate": true, + "data_source_global": { + "dataHandler": { + "type": "JSFunction", + "value": "function dataHanlder(res){\n return res;\n}" + } + } + }, + "name": "我的分类", + "desc": "", + "blocks": [ + "ALvDb0JD8atzd3nA" + ], + "category_id": "qukuaifenlei", + "_id": "L0fyFYECrNiRZMiX" +} \ No newline at end of file diff --git "a/mockServer/data/blockGroups/\346\210\221\347\232\204\345\214\272\345\235\227.json" "b/mockServer/data/blockGroups/\346\210\221\347\232\204\345\214\272\345\235\227.json" new file mode 100644 index 0000000000..3b97abe2c4 --- /dev/null +++ "b/mockServer/data/blockGroups/\346\210\221\347\232\204\345\214\272\345\235\227.json" @@ -0,0 +1,85 @@ +{ + "id": "b57MCCORYPGjgL23", + "app": { + "id": 1, + "name": "portal-app", + "app_website": null, + "platform": { + "id": 897, + "name": "portal-platform" + }, + "obs_url": "", + "created_by": null, + "updated_by": null, + "created_at": "2022-06-08T07:19:01.000Z", + "updated_at": "2023-09-04T08:55:40.000Z", + "state": null, + "published": false, + "createdBy": 86, + "updatedBy": 564, + "tenant": 1, + "home_page": "1", + "css": null, + "config": {}, + "git_group": "", + "project_name": "", + "constants": null, + "data_handler": { + "type": "JSFunction", + "value": "function dataHanlder(res){\n return res;\n}" + }, + "description": "demo应用", + "latest": 22, + "platform_history": null, + "editor_url": "", + "branch": "develop", + "visit_url": null, + "is_demo": null, + "image_url": "", + "is_default": true, + "template_type": null, + "set_template_time": null, + "set_template_by": null, + "set_default_by": 169, + "framework": "Vue", + "global_state": [], + "default_lang": null, + "extend_config": { + "business": { + "serviceName": "", + "endpointName": "cce", + "endpointId": "ee", + "serviceId": "ee", + "router": "ee" + }, + "env": { + "alpha": { + "regions": [ + { + "name": "", + "baseUrl": "", + "isDefault": false + } + ], + "isDefault": true + } + }, + "type": "console" + }, + "assets_url": "", + "data_hash": "ae128e37f6bc378f1b9c21d75bd05551", + "can_associate": true, + "data_source_global": { + "dataHandler": { + "type": "JSFunction", + "value": "function dataHanlder(res){\n return res;\n}" + } + } + }, + "name": "我的区块", + "desc": "", + "blocks": [ + "ALvDb0JD8atzd3nA" + ], + "_id": "b57MCCORYPGjgL23" +} \ No newline at end of file diff --git a/mockServer/data/blocks/PortalBlock.json b/mockServer/data/blocks/PortalBlock.json new file mode 100644 index 0000000000..269c3a91e3 --- /dev/null +++ b/mockServer/data/blocks/PortalBlock.json @@ -0,0 +1,135 @@ +{ + "id": "V85zd9sWEya25Kxh", + "label": "PortalBlock", + "name_cn": null, + "framework": "Vue", + "content": { + "state": {}, + "methods": {}, + "componentName": "Block", + "fileName": "PortalBlock", + "css": "", + "props": {}, + "children": [ + { + "componentName": "div", + "props": { + "style": "font-size: 18px; height: 40px; border-bottom: 1px solid rgb(223, 225, 230); margin-top: 20px;" + }, + "id": "d38cea57", + "children": [ + { + "componentName": "Icon", + "props": { + "name": "IconChevronLeft" + }, + "id": "86c6e6b0" + }, + { + "componentName": "Text", + "props": { + "text": "编辑物料资产包 | ", + "style": "margin-left: 10px; font-weight: bold;" + }, + "id": "38d9fbc8" + }, + { + "componentName": "Text", + "props": { + "text": { + "type": "JSExpression", + "value": "this.props.blockName" + }, + "style": "margin-left: 10px; font-weight: bold;" + }, + "id": "6cd76396" + } + ] + } + ], + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "blockName", + "type": "String", + "defaultValue": "MT0526-React 1.0", + "label": { + "text": { + "zh_CN": "区块名称" + } + }, + "cols": 12, + "rules": [], + "handle": { + "getter": "", + "setter": "" + }, + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "MetaInput", + "props": { + "modelValue": "MT0526-React 1.0" + } + } + } + ] + } + ], + "events": {}, + "slots": {} + }, + "dataSource": {} + }, + "description": null, + "path": "portal", + "screenshot": "", + "created_app": null, + "tags": null, + "categories": [], + "occupier": { + "id": 86, + "username": "开发者" + }, + "isDefault": null, + "isOfficial": null, + "created_at": "2022-06-28T08:59:54.000Z", + "updated_at": "2023-01-13T08:20:09.000Z", + "assets": { + "material": [], + "scripts": [ + "http://localhost:9090/assets/js/1005web-components.es.js", + "http://localhost:9090/assets/js/1005web-components.umd.js" + ], + "styles": [] + }, + "createdBy": { + "id": 86, + "username": "开发者" + }, + "current_history": 1665, + "public": 1, + "tiny_reserved": false, + "author": null, + "content_blocks": null, + "public_scope_tenants": [], + "histories_length": 1, + "is_published": true, + "_id": "V85zd9sWEya25Kxh" +} \ No newline at end of file diff --git a/mockServer/data/blocks/PortalHome.json b/mockServer/data/blocks/PortalHome.json new file mode 100644 index 0000000000..337c0d9537 --- /dev/null +++ b/mockServer/data/blocks/PortalHome.json @@ -0,0 +1,302 @@ +{ + "id": "ALvDb0JD8atzd3nA", + "label": "PortalHome", + "name_cn": null, + "framework": "Vue", + "content": { + "state": { + "logoUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXwAAAF8CAYAAADM5wDKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjI5OEVGOTU4RTg2NDExRUM5MDhERjU4NjRDOUUxQTUwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjI5OEVGOTU5RTg2NDExRUM5MDhERjU4NjRDOUUxQTUwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6Mjk4RUY5NTZFODY0MTFFQzkwOERGNTg2NEM5RTFBNTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6Mjk4RUY5NTdFODY0MTFFQzkwOERGNTg2NEM5RTFBNTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4dZkpJAAAvNElEQVR42uyd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", + "loginImgUrl": "data:image/png;base64,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" + }, + "componentName": "Block", + "css": ".home-content {\r\n display: flex;\r\n flex-direction: column;\r\n justify-content: center;\r\n align-items: center;\r\n text-align: center;\r\n height: calc(100vh - 262px);\r\n \r\n}\r\n.home-content .btn {\r\n margin-top: 24px;\r\n \r\n }\r\n .home-content .btn button {\r\n border: none;\r\n border-radius: 30px;\r\n background: #5e7ce0;\r\n \r\n font-size: 14px;\r\n color: #fff;\r\n \r\n cursor: pointer;\r\n }\r\n\r\n .home-content .text {\r\n font-size: 18px;\r\n }\r\n\r\n .home-content .account {\r\n margin-top: 16px;\r\n \r\n \r\n }\r\n\r\n .home-content .account .sub-text {\r\n color: #575d6c;\r\n }\r\n .home-content .account .login {\r\n color: #1890ff;\r\n cursor: pointer;\r\n }\r\n .home-content .logo img{\r\n border-radius: 50%;\r\n overflow: hidden;\r\n }", + "props": {}, + "children": [ + { + "componentName": "div", + "props": { + "className": "home", + "style": "height: 100vh; display: flex;" + }, + "id": "357534ab", + "children": [ + { + "componentName": "TinyRow", + "props": { + "align": "middle", + "flex": true, + "style": "" + }, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": 6, + "style": "text-align: center; display: flex; justify-content: center;" + }, + "id": "f01b66ea", + "children": [ + { + "componentName": "div", + "props": { + "style": "width: 90%; height: 50%;" + }, + "id": "8197d016", + "children": [ + { + "componentName": "Img", + "props": { + "style": "width: 100%; height: 100%;", + "src": { + "type": "JSExpression", + "value": "this.state.loginImgUrl" + } + }, + "id": "471e30f3" + } + ] + } + ] + }, + { + "componentName": "TinyCol", + "props": { + "span": "6", + "style": "text-align: center;" + }, + "id": "781d5b46", + "children": [ + { + "componentName": "div", + "props": { + "className": "home-content", + "style": "font-size: 14px;" + }, + "id": "08638b8a", + "children": [ + { + "componentName": "div", + "props": { + "className": "text" + }, + "id": "18712ee2", + "children": [ + { + "componentName": "div", + "props": { + "style": "font-size: 16px;" + }, + "id": "07e6794c", + "children": [ + { + "componentName": "div", + "props": { + "className": "logo" + }, + "id": "07cad264", + "children": [ + { + "componentName": "Img", + "props": { + "style": "width: 105px; height: 105px; border-radius: 100px;", + "src": { + "type": "JSExpression", + "value": "this.state.logoUrl" + } + }, + "id": "f4489e27" + } + ] + }, + { + "componentName": "Text", + "props": { + "text": "TinyLowCode 低代码平台", + "style": "display: block; font-size: 28px; margin-top: 12px; margin-bottom: 12px; font-weight: bold;", + "ref": "", + "className": "title" + }, + "id": "e82108ce" + }, + { + "componentName": "Text", + "props": { + "text": "致力于通过友好的用户交互提升业务的开发效率", + "style": "display: block; margin-bottom: 12px;" + }, + "id": "65a2f1ad" + }, + { + "componentName": "Text", + "props": { + "text": "欢迎一起来解锁~~", + "style": "margin-top: 12px;" + }, + "id": "bb879abb" + } + ] + }, + { + "componentName": "div", + "props": { + "className": "btn" + }, + "id": "44b2bcbd", + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": "立即体验", + "round": true, + "type": "primary", + "style": "margin-top: 40px;" + }, + "id": "9580c5e7" + }, + { + "componentName": "div", + "props": { + "className": "account" + }, + "id": "6a8ffa3e", + "children": [ + { + "componentName": "div", + "props": { + "style": "font-size: 14px; margin-top: 4px;" + }, + "id": "bfc6eb6c", + "children": [ + { + "componentName": "Text", + "props": { + "text": "已有团队?", + "style": "color: #777777;" + }, + "id": "3d993264" + }, + { + "componentName": "Text", + "props": { + "text": "立即进入", + "style": "color: #5e7ce0;", + "onClick": { + "type": "JSExpression", + "value": "this.handleClick(event)" + } + }, + "id": "21390118" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ], + "id": "4545fea2" + } + ] + } + ], + "methods": { + "handleClick": { + "type": "JSFunction", + "value": "function (event) {this.emit('goto-home', event)\n}" + } + }, + "fileName": "PortalHome", + "meta": { + "id": 1722, + "parentId": "0", + "group": "staticPages", + "title": null, + "occupier": null, + "isHome": false, + "description": "", + "router": "/", + "rootElement": "div", + "creator": "开发者", + "gmt_create": "2022-06-08 03:25:51", + "gmt_modified": "2022-06-09 05:19:09" + }, + "id": 1722, + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [] + } + ], + "events": { + "onGotoHome": { + "label": { + "zh_CN": "点击立即进入触发方法" + }, + "description": { + "zh_CN": "点击立即进入触发方法" + }, + "type": "event", + "functionInfo": { + "params": [], + "returns": {} + }, + "defaultValue": "", + "linked": { + "id": "21390118", + "componentName": "Text", + "event": "onClick" + } + } + }, + "slots": {} + }, + "dataSource": {}, + "i18n": {} + }, + "description": null, + "path": "common/components/home", + "screenshot": "", + "created_app": null, + "tags": "", + "categories": [], + "occupier": null, + "isDefault": null, + "isOfficial": true, + "created_at": "2022-06-13T07:56:51.000Z", + "updated_at": "2023-01-13T08:12:51.000Z", + "assets": { + "material": [], + "scripts": [ + "http://localhost:9090/assets/js/989web-components.es.js", + "http://localhost:9090/assets/js/989web-components.umd.js" + ], + "styles": [] + }, + "createdBy": 86, + "current_history": 1655, + "public": 1, + "tiny_reserved": false, + "author": null, + "content_blocks": null, + "current_version": "x", + "is_published": true, + "_id": "ALvDb0JD8atzd3nA" +} \ No newline at end of file diff --git a/mockServer/data/pages/CreateVm.json b/mockServer/data/pages/CreateVm.json new file mode 100644 index 0000000000..76708f8ae2 --- /dev/null +++ b/mockServer/data/pages/CreateVm.json @@ -0,0 +1,960 @@ +{ + "name": "CreateVm", + "id": "1", + "app": "1", + "route": "CreateVm", + "page_content": { + "state": { + "dataDisk": [ + 1, + 2, + 3 + ] + }, + "methods": {}, + "componentName": "Page", + "css": "body {\r\n background-color:#eef0f5 ;\r\n margin-bottom: 80px;\r\n}", + "props": {}, + "children": [ + { + "componentName": "div", + "props": { + "style": "padding-bottom: 10px; padding-top: 10px;" + }, + "id": "2b2cabf0", + "children": [ + { + "componentName": "TinyTimeLine", + "props": { + "active": "2", + "data": [ + { + "name": "基础配置" + }, + { + "name": "网络配置" + }, + { + "name": "高级配置" + }, + { + "name": "确认配置" + } + ], + "horizontal": true, + "style": "border-radius: 0px;" + }, + "id": "dd764b17" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "id": "30c94cc8", + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "计费模式" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "包年/包月", + "value": "1" + }, + { + "text": "按需计费", + "value": "2" + } + ], + "modelValue": "1" + }, + "id": "a8d84361" + } + ], + "id": "9f39f3e7" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "区域" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "乌兰察布二零一", + "value": "1" + } + ], + "modelValue": "1", + "style": "border-radius: 0px; margin-right: 10px;" + }, + "id": "c97ccd99" + }, + { + "componentName": "Text", + "props": { + "text": "温馨提示:页面左上角切换区域", + "style": "background-color: #f5f5f5; color: #8a8e99; font-size: 12px;" + }, + "id": "20923497" + }, + { + "componentName": "Text", + "props": { + "text": "不同区域的云服务产品之间内网互不相通;请就近选择靠近您业务的区域,可减少网络时延,提高访问速度", + "style": "display: block; color: #8a8e99; border-radius: 0px; font-size: 12px;" + }, + "id": "54780a26" + } + ], + "id": "4966384d" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "可用区", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "可用区1", + "value": "1" + }, + { + "text": "可用区2", + "value": "2" + }, + { + "text": "可用区3", + "value": "3" + } + ], + "modelValue": "1" + }, + "id": "6184481b" + } + ], + "id": "690837bf" + } + ], + "id": "b6a425d4" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "CPU架构" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "x86计算", + "value": "1" + }, + { + "text": "鲲鹏计算", + "value": "2" + } + ], + "modelValue": "1" + }, + "id": "7d33ced7" + } + ], + "id": "05ed5a79" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "区域" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex; justify-content: flex-start; align-items: center;" + }, + "id": "606edf78", + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center; margin-right: 10px;" + }, + "id": "f3f98246", + "children": [ + { + "componentName": "Text", + "props": { + "text": "vCPUs", + "style": "width: 80px;" + }, + "id": "c287437e" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "id": "4c43286b" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center; margin-right: 10px;" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "内存", + "style": "width: 80px; border-radius: 0px;" + }, + "id": "38b8fa1f" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "id": "cd33328e" + } + ], + "id": "2b2c678f" + }, + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center;" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "规格名称", + "style": "width: 80px;" + }, + "id": "d3eb6352" + }, + { + "componentName": "TinySearch", + "props": { + "modelValue": "", + "placeholder": "输入关键词" + }, + "id": "21cb9282" + } + ], + "id": "b8e0f35c" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "border-radius: 0px;" + }, + "id": "5000c83e", + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "通用计算型", + "value": "1" + }, + { + "text": "通用计算增强型", + "value": "2" + }, + { + "text": "内存优化型", + "value": "3" + }, + { + "text": "内存优化型", + "value": "4" + }, + { + "text": "磁盘增强型", + "value": "5" + }, + { + "text": "超高I/O型", + "value": "6" + }, + { + "text": "GPU加速型", + "value": "7" + } + ], + "modelValue": "1", + "style": "border-radius: 0px; margin-top: 12px;" + }, + "id": "b8724703" + }, + { + "componentName": "TinyGrid", + "props": { + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "columns": [ + { + "type": "radio", + "width": 60 + }, + { + "field": "employees", + "title": "规格名称" + }, + { + "field": "created_date", + "title": "vCPUs | 内存(GiB)", + "sortable": true + }, + { + "field": "city", + "title": "CPU", + "sortable": true + }, + { + "title": "基准 / 最大带宽\t", + "sortable": true + }, + { + "title": "内网收发包", + "sortable": true + } + ], + "data": [ + { + "id": "1", + "name": "GFD科技有限公司", + "city": "福州", + "employees": 800, + "created_date": "2014-04-30 00:56:00", + "boole": false + }, + { + "id": "2", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true + } + ], + "style": "margin-top: 12px; border-radius: 0px;", + "auto-resize": true + }, + "id": "77701c25" + }, + { + "componentName": "div", + "props": { + "style": "margin-top: 12px; border-radius: 0px;" + }, + "id": "3339838b", + "children": [ + { + "componentName": "Text", + "props": { + "text": "当前规格", + "style": "width: 150px; display: inline-block;" + }, + "id": "203b012b" + }, + { + "componentName": "Text", + "props": { + "text": "通用计算型 | Si2.large.2 | 2vCPUs | 4 GiB", + "style": "font-weight: 700;" + }, + "id": "87723f52" + } + ] + } + ] + } + ], + "id": "657fb2fc" + } + ], + "id": "d19b15cf" + } + ], + "id": "9991228b" + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "镜像", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "公共镜像", + "value": "1" + }, + { + "text": "私有镜像", + "value": "2" + }, + { + "text": "共享镜像", + "value": "3" + } + ], + "modelValue": "1" + }, + "id": "922b14cb" + }, + { + "componentName": "div", + "props": { + "style": "display: flex; margin-top: 12px; border-radius: 0px;" + }, + "id": "6b679524", + "children": [ + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 170px; margin-right: 10px;" + }, + "id": "4851fff7" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 340px;" + }, + "id": "a7183eb7" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "margin-top: 12px;" + }, + "id": "57aee314", + "children": [ + { + "componentName": "Text", + "props": { + "text": "请注意操作系统的语言类型。", + "style": "color: #e37d29;" + }, + "id": "56d36c27" + } + ] + } + ], + "id": "e3b02436" + } + ], + "id": "59aebf2b" + } + ], + "id": "87ff7b99" + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "系统盘", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex;" + }, + "id": "cddba5b8", + "children": [ + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 200px; margin-right: 10px;" + }, + "id": "a97fbe15" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px;" + }, + "id": "1cde4c0f" + }, + { + "componentName": "Text", + "props": { + "text": "GiB \nIOPS上限240,IOPS突发上限5,000", + "style": "color: #575d6c; font-size: 12px;" + }, + "id": "2815d82d" + } + ] + } + ], + "id": "50239a3a" + } + ], + "id": "e8582986" + }, + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "数据盘", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "margin-top: 12px; display: flex;" + }, + "id": "728c9825", + "children": [ + { + "componentName": "Icon", + "props": { + "style": "margin-right: 10px; width: 16px; height: 16px;", + "name": "IconPanelMini" + }, + "id": "fded6930" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 200px; margin-right: 10px;" + }, + "id": "62734e3f" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px;" + }, + "id": "667c7926" + }, + { + "componentName": "Text", + "props": { + "text": "GiB \nIOPS上限600,IOPS突发上限5,000", + "style": "color: #575d6c; font-size: 12px; margin-right: 10px;" + }, + "id": "e7bc36d6" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px;" + }, + "id": "1bd56dc0" + } + ], + "loop": { + "type": "JSExpression", + "value": "this.state.dataDisk" + } + }, + { + "componentName": "div", + "props": { + "style": "display: flex; margin-top: 12px; border-radius: 0px;" + }, + "children": [ + { + "componentName": "Icon", + "props": { + "name": "IconPlus", + "style": "width: 16px; height: 16px; margin-right: 10px;" + }, + "id": "65c89f2b" + }, + { + "componentName": "Text", + "props": { + "text": "增加一块数据盘", + "style": "font-size: 12px; border-radius: 0px; margin-right: 10px;" + }, + "id": "cb344071" + }, + { + "componentName": "Text", + "props": { + "text": "您还可以挂载 21 块磁盘(云硬盘)", + "style": "color: #8a8e99; font-size: 12px;" + }, + "id": "80eea996" + } + ], + "id": "e9e530ab" + } + ], + "id": "078e03ef" + } + ], + "id": "ccef886e" + } + ], + "id": "0fb7bd74" + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-color: #ffffff; padding-top: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; position: fixed; inset: auto 0% 0% 0%; height: 80px; line-height: 80px; border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [], + "id": "21ed4475" + }, + { + "componentName": "TinyRow", + "props": { + "style": "border-radius: 0px; height: 100%;" + }, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": "8" + }, + "id": "b9d051a5", + "children": [ + { + "componentName": "TinyRow", + "props": { + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": "5", + "style": "display: flex;" + }, + "id": "02352776", + "children": [ + { + "componentName": "Text", + "props": { + "text": "购买量", + "style": "margin-right: 10px;" + }, + "id": "0cd9ed5c" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px;" + }, + "id": "2f9cf442" + }, + { + "componentName": "Text", + "props": { + "text": "台" + }, + "id": "facd4481" + } + ] + }, + { + "componentName": "TinyCol", + "props": { + "span": "7" + }, + "id": "82b6c659", + "children": [ + { + "componentName": "div", + "props": {}, + "id": "9cd65874", + "children": [ + { + "componentName": "Text", + "props": { + "text": "配置费用", + "style": "font-size: 12px;" + }, + "id": "b5a0a0da" + }, + { + "componentName": "Text", + "props": { + "text": "¥1.5776", + "style": "padding-left: 10px; padding-right: 10px; color: #de504e;" + }, + "id": "d9464214" + }, + { + "componentName": "Text", + "props": { + "text": "/小时", + "style": "font-size: 12px;" + }, + "id": "af7cc5e6" + } + ] + }, + { + "componentName": "div", + "props": {}, + "id": "89063830", + "children": [ + { + "componentName": "Text", + "props": { + "text": "参考价格,具体扣费请以账单为准。", + "style": "font-size: 12px; border-radius: 0px;" + }, + "id": "d8995fbc" + }, + { + "componentName": "Text", + "props": { + "text": "了解计费详情", + "style": "font-size: 12px; color: #344899;" + }, + "id": "b383c3e2" + } + ] + } + ] + } + ], + "id": "94fc0e43" + } + ] + }, + { + "componentName": "TinyCol", + "props": { + "span": "4", + "style": "display: flex; flex-direction: row-reverse; border-radius: 0px; height: 100%; justify-content: flex-start; align-items: center;" + }, + "id": "10b73009", + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": "下一步: 网络配置", + "type": "danger", + "style": "max-width: unset;" + }, + "id": "0b584011" + } + ] + } + ], + "id": "d414a473" + } + ], + "id": "e8ec029b" + } + ], + "fileName": "CreateVm" + }, + "tenant": 1, + "isBody": false, + "parentId": "0", + "group": "staticPages", + "depth": 0, + "isPage": true, + "isDefault": false, + "occupier": null, + "isHome": false, + "_id": "1" +} \ No newline at end of file diff --git a/mockServer/gulpfile.js b/mockServer/gulpfile.js index a2a99308c1..fdc8750e4e 100644 --- a/mockServer/gulpfile.js +++ b/mockServer/gulpfile.js @@ -53,7 +53,7 @@ gulp.task( return [] }, verbose: true, - ignore: ['build/*.js', 'dist/*.js', 'nodemon.json', '.git', 'node_modules/**/node_modules', 'gulpfile.js'], + ignore: ['build/*.js', 'dist/*.js', 'nodemon.json', '.git', 'node_modules/**/node_modules', 'gulpfile.js', 'data/*'], env: { NODE_ENV: 'development' }, @@ -77,7 +77,7 @@ gulp.task('nodemon', () => { js: jsScript }, verbose: true, - ignore: ['build/*.js', 'dist/*.js', 'nodemon.json', '.git', 'node_modules/**/node_modules', 'gulpfile.js'], + ignore: ['build/*.js', 'dist/*.js', 'nodemon.json', '.git', 'node_modules/**/node_modules', 'gulpfile.js', 'data/*'], env: { NODE_ENV: 'development' }, @@ -92,7 +92,7 @@ gulp.task('default', () => { js: jsScript }, verbose: true, - ignore: ['build/*.js', 'dist/*.js', 'nodemon.json', '.git', 'node_modules/**/node_modules', 'gulpfile.js'], + ignore: ['build/*.js', 'dist/*.js', 'nodemon.json', '.git', 'node_modules/**/node_modules', 'gulpfile.js', 'data/*'], env: { NODE_ENV: 'development' }, diff --git a/mockServer/package.json b/mockServer/package.json index cefcc7092c..3791f7d1a5 100644 --- a/mockServer/package.json +++ b/mockServer/package.json @@ -24,6 +24,8 @@ "scripts": { "start": "gulp nodemon", "dev": "gulp", + "dev:file": "cross-env MOCK_DB_MODE=file gulp", + "export-db-to-file": "node scripts/export-db-to-file.js", "build:js": "babel src --out-dir dist", "build:static": "ncp src/assets dist/assets && ncp src/mock dist/mock && ncp src/database dist/database", "build": "npm run build:js && npm run build:static", @@ -33,6 +35,7 @@ "dependencies": { "@babel/runtime": "^7.9.2", "@seald-io/nedb": "^4.0.2", + "cross-env": "^7.0.3", "fs-extra": "^11.1.1", "glob": "^10.3.4", "koa": "^2.11.0", diff --git a/mockServer/scripts/export-db-to-file.js b/mockServer/scripts/export-db-to-file.js new file mode 100644 index 0000000000..0ba06cabb4 --- /dev/null +++ b/mockServer/scripts/export-db-to-file.js @@ -0,0 +1,192 @@ +#!/usr/bin/env node + +const fs = require('fs') +const path = require('path') + +const sourceDir = path.resolve(__dirname, '../src/database') +const outputDir = process.env.MOCK_FILE_DB_PATH || path.resolve(__dirname, '../data') +const forceOverwrite = process.argv.includes('--force') + +const collectionNamingFields = { + pages: ['name'], + apps: ['name'], + appsSchema: ['id'], + blocks: ['label', 'name'], + blockGroups: ['name'], + blockCategories: ['name'] +} + +function isNedbMetadataRecord (doc) { + if (!doc || typeof doc !== 'object' || Array.isArray(doc)) { + return false + } + + return Object.keys(doc).some((key) => key.startsWith('$$')) +} + +function sanitizeFileName (name) { + if (!name) { + return '' + } + + const normalized = String(name) + .trim() + .replace(/\s+/g, '-') + .replace(/[<>:"/\\|?*]/g, '-') + .replace(/-+/g, '-') + .replace(/^\.+/, '') + .replace(/\.+$/, '') + .slice(0, 120) + + if (!normalized || normalized === '.' || normalized === '..') { + return '' + } + + return normalized +} + +function resolveFileBaseName (doc, collectionName, usedNames) { + const namingFields = collectionNamingFields[collectionName] || ['name', 'label'] + let preferred = '' + + for (const field of namingFields) { + const value = sanitizeFileName(doc[field]) + if (value) { + preferred = value + break + } + } + + if (!preferred) { + preferred = sanitizeFileName(doc.id) || sanitizeFileName(doc._id) || 'record' + } + + const idSuffix = String(doc._id || doc.id || 'item').slice(0, 6) + const normalizedPreferred = preferred.toLowerCase() + if (!usedNames.has(normalizedPreferred)) { + usedNames.add(normalizedPreferred) + return preferred + } + + let attempt = `${preferred}-${idSuffix}` + let seq = 2 + + while (usedNames.has(attempt.toLowerCase())) { + attempt = `${preferred}-${idSuffix}-${seq}` + seq += 1 + } + + usedNames.add(attempt.toLowerCase()) + return attempt +} + +function parseDbFile (dbPath) { + const content = fs.readFileSync(dbPath, 'utf8') + const docs = content + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line, index) => { + try { + return JSON.parse(line) + } catch (error) { + throw new Error(`Failed to parse ${path.basename(dbPath)} line ${index + 1}: ${error.message}`) + } + }) + + const filteredDocs = docs.filter((doc) => !isNedbMetadataRecord(doc)) + + return { + docs: filteredDocs, + filtered: docs.length - filteredDocs.length + } +} + +function ensureDirectory (dirPath) { + fs.mkdirSync(dirPath, { recursive: true }) +} + +function cleanCollectionDirectory (collectionPath) { + if (!fs.existsSync(collectionPath)) { + return + } + + const files = fs.readdirSync(collectionPath) + for (const fileName of files) { + if (fileName.endsWith('.json')) { + fs.unlinkSync(path.join(collectionPath, fileName)) + } + } +} + +function exportCollection (dbFile) { + const collectionName = path.basename(dbFile, '.db') + const dbPath = path.join(sourceDir, dbFile) + const collectionPath = path.join(outputDir, collectionName) + const { docs, filtered } = parseDbFile(dbPath) + + ensureDirectory(collectionPath) + if (forceOverwrite) { + cleanCollectionDirectory(collectionPath) + } + + let written = 0 + let skipped = 0 + const usedNames = new Set() + + for (const doc of docs) { + if (!doc._id && doc.id !== undefined) { + doc._id = String(doc.id) + } + + const fileBaseName = resolveFileBaseName(doc, collectionName, usedNames) + const filePath = path.join(collectionPath, `${fileBaseName}.json`) + + if (!forceOverwrite && fs.existsSync(filePath)) { + skipped += 1 + continue + } + + fs.writeFileSync(filePath, JSON.stringify(doc, null, 2), 'utf8') + written += 1 + } + + return { + collectionName, + total: docs.length + filtered, + filtered, + written, + skipped + } +} + +function run () { + if (!fs.existsSync(sourceDir)) { + throw new Error(`Database directory not found: ${sourceDir}`) + } + + ensureDirectory(outputDir) + + const dbFiles = fs.readdirSync(sourceDir).filter((fileName) => fileName.endsWith('.db')) + + if (dbFiles.length === 0) { + console.log('No .db files found, nothing to export.') + return + } + + const summary = dbFiles.map(exportCollection) + + console.log(`Export complete. Output: ${outputDir}`) + summary.forEach((item) => { + console.log( + `- ${item.collectionName}: total=${item.total}, filtered=${item.filtered}, written=${item.written}, skipped=${item.skipped}` + ) + }) +} + +try { + run() +} catch (error) { + console.error(error.message) + process.exit(1) +} diff --git a/mockServer/src/config/config.js b/mockServer/src/config/config.js index dcd70e9a14..db0b739917 100644 --- a/mockServer/src/config/config.js +++ b/mockServer/src/config/config.js @@ -10,7 +10,11 @@ * */ +const path = require('path') + module.exports = { port: process.env.MOCK_PORT || 9090, - env: process.env.NODE_ENV || 'development' // Current mode + env: process.env.NODE_ENV || 'development', // Current mode + dbMode: process.env.MOCK_DB_MODE || 'db', // 'db' or 'file' + fileDbPath: process.env.MOCK_FILE_DB_PATH || path.resolve(__dirname, '../../data') } diff --git a/mockServer/src/services/apps.js b/mockServer/src/services/apps.js index e84b683fc8..8a5fff5952 100644 --- a/mockServer/src/services/apps.js +++ b/mockServer/src/services/apps.js @@ -10,8 +10,8 @@ * */ -import DateStore from '@seald-io/nedb' -import { getDatabasePath, getResponseData } from '../tool/Common' +import createStore from '../store/StoreFactory' +import { getResponseData } from '../tool/Common' import defaultAppSchema from '../mock/get/app-center/v1/apps/schema/16.json' const defaultApp = { @@ -63,39 +63,30 @@ const defaultApp = { export default class AppsService { constructor() { - this.db = new DateStore({ - filename: getDatabasePath('apps.db'), - autoload: true + this.store = createStore('apps', { + indexes: [{ fieldName: '_id', unique: true }], + namingFields: ['name'] }) - this.db.ensureIndex({ - fieldName: '_id', - unique: true - }) - - this.schemaDb = new DateStore({ - filename: getDatabasePath('appsSchema.db'), - autoload: true - }) - - this.schemaDb.ensureIndex({ - fieldName: '_id', - unique: true + this.schemaStore = createStore('appsSchema', { + indexes: [{ fieldName: '_id', unique: true }], + namingFields: ['id'] }) this.appList = [] } async create(params) { - let mockId = this.appList.length > 0 ? Math.max(...this.appList.map((item) => item.id)) + 1 : 3 + const all = await this.store.find({}) + const mockId = all.length > 0 ? Math.max(...all.map((item) => Number(item.id) || 0)) + 1 : 3 const newApp = { ...defaultApp, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), - id: mockId++, + id: mockId, ...params } - this.db.insert(newApp) + await this.store.insert(newApp) let resultStr = JSON.stringify(defaultAppSchema.data) resultStr = resultStr.replace(/"lowcode./g, '"lowcode_') @@ -112,15 +103,27 @@ export default class AppsService { }, id: newApp.id } - this.schemaDb.insert(newAppSchema) + // App and schema live in different collections. If the schema write fails, + // best-effort roll back the app insert so we don't leave a one-sided record, + // then rethrow — ErrorRoutesCatch serializes thrown errors into the response. + try { + await this.schemaStore.insert(newAppSchema) + } catch (err) { + try { + await this.store.remove({ id: newApp.id }) + } catch { + /* swallow cleanup failure; surface the original error below */ + } + throw err + } return getResponseData(newApp) } async delete(id) { - const result = await this.db.findOneAsync({ id: Number(id) }) - await this.db.removeAsync({ id: Number(id) }) + const result = await this.store.findOne({ id: Number(id) }) + await this.store.remove({ id: Number(id) }) - await this.schemaDb.removeAsync({ id: Number(id) }) + await this.schemaStore.remove({ id: Number(id) }) return getResponseData(result) } @@ -131,7 +134,7 @@ export default class AppsService { query.name = { $regex: new RegExp(name, 'i') } } - const result = await this.db.findAsync(query) + const result = await this.store.find(query) this.appList = result.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)) if (createdBy) { @@ -146,18 +149,18 @@ export default class AppsService { } async update(id, params) { - await this.db.updateAsync({ id: Number(id) }, { $set: params }) - const result = await this.db.findOneAsync({ id: Number(id) }) + await this.store.update({ id: Number(id) }, { $set: params }) + const result = await this.store.findOne({ id: Number(id) }) return getResponseData(result) } async find(id) { - const result = await this.db.findOneAsync({ id: Number(id) }) + const result = await this.store.findOne({ id: Number(id) }) return getResponseData(result) } async findSchema(id) { - const result = await this.schemaDb.findOneAsync({ id: Number(id) }) + const result = await this.schemaStore.findOne({ id: Number(id) }) let resultStr = JSON.stringify(result) resultStr = resultStr.replace(/"lowcode_/g, '"lowcode.') const modifiedResult = JSON.parse(resultStr) diff --git a/mockServer/src/services/block.js b/mockServer/src/services/block.js index 12807c2392..78e3734691 100644 --- a/mockServer/src/services/block.js +++ b/mockServer/src/services/block.js @@ -9,26 +9,21 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ -import DateStore from '@seald-io/nedb' -import { getDatabasePath, getResponseData } from '../tool/Common' +import createStore from '../store/StoreFactory' +import { getResponseData } from '../tool/Common' export default class BlockService { constructor() { - this.db = new DateStore({ - filename: getDatabasePath('blocks.db'), - autoload: true - }) - - this.db.ensureIndex({ - fieldName: 'label', - unique: true + this.store = createStore('blocks', { + indexes: [{ fieldName: 'label', unique: true }], + namingFields: ['label', 'name'] }) this.userInfo = { id: 86, username: '开发者', - email: 'developer@lowcode.com', - confirmationToken: 'dfb2c162-351f-4f44-ad5f-8998', + email: 'demo@example.com', + confirmationToken: null, is_admin: true } @@ -55,38 +50,38 @@ export default class BlockService { async create(params) { const blockData = { ...this.blockModel, ...params } - const result = await this.db.insertAsync(blockData) + const result = await this.store.insert(blockData) const { _id } = result - await this.db.updateAsync({ _id }, { $set: { id: _id } }) + await this.store.update({ _id }, { $set: { id: _id } }) result.id = result._id return result } async update(id, params) { - await this.db.updateAsync({ _id: id }, { $set: params }) - const result = await this.db.findOneAsync({ _id: id }) + await this.store.update({ _id: id }, { $set: params }) + const result = await this.store.findOne({ _id: id }) return getResponseData(result) } async detail(blockId) { - const result = await this.db.findOneAsync({ _id: blockId }) + const result = await this.store.findOne({ _id: blockId }) return getResponseData(result) } async delete(blockId) { - const result = await this.db.findOneAsync({ _id: blockId }) - await this.db.removeAsync({ _id: blockId }) + const result = await this.store.findOne({ _id: blockId }) + await this.store.remove({ _id: blockId }) return getResponseData(result) } async list(appId) { - const result = await this.db.findAsync() + const result = await this.store.find() return getResponseData(result) } async find(params) { - const result = await this.db.findAsync(params) + const result = await this.store.find(params) return result } } diff --git a/mockServer/src/services/blockCategory.js b/mockServer/src/services/blockCategory.js index 2c6da89306..9f9b931ec8 100644 --- a/mockServer/src/services/blockCategory.js +++ b/mockServer/src/services/blockCategory.js @@ -9,20 +9,15 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ -import DateStore from '@seald-io/nedb' -import { getDatabasePath, getResponseData } from '../tool/Common' +import createStore from '../store/StoreFactory' +import { getResponseData } from '../tool/Common' import appinfo from '../assets/json/appinfo.json' export default class BlockCategoryService { constructor() { - this.db = new DateStore({ - filename: getDatabasePath('blockCategories.db'), - autoload: true - }) - - this.db.ensureIndex({ - fieldName: 'name', - unique: true + this.store = createStore('blockCategories', { + indexes: [{ fieldName: 'name', unique: true }], + namingFields: ['name'] }) this.blockCategoriesModel = { @@ -37,42 +32,42 @@ export default class BlockCategoryService { async create(params) { const blockCategoriesData = { ...this.blockCategoriesModel, ...params } blockCategoriesData.app = appinfo.app - const result = await this.db.insertAsync(blockCategoriesData) + const result = await this.store.insert(blockCategoriesData) const { _id } = result - await this.db.updateAsync({ _id }, { $set: { id: _id } }) + await this.store.update({ _id }, { $set: { id: _id } }) result.id = result._id return getResponseData(result) } async update(id, params) { if (params?._id) { - const categories = await this.db.findOneAsync({ _id: id }) + const categories = await this.store.findOne({ _id: id }) if (categories) { categories.blocks.push(params._id) - await this.db.updateAsync({ _id: id }, { $set: categories }) + await this.store.update({ _id: id }, { $set: categories }) return getResponseData(categories) } } params.app = appinfo.app - await this.db.updateAsync({ _id: id }, { $set: params }) + await this.store.update({ _id: id }, { $set: params }) - const result = await this.db.findOneAsync({ _id: id }) + const result = await this.store.findOne({ _id: id }) return getResponseData(result) } async find(params) { - const result = await this.db.findAsync() + const result = await this.store.find() return getResponseData(result) } async delete(id) { - const result = await this.db.findOneAsync({ _id: id }) - await this.db.removeAsync({ _id: id }) + const result = await this.store.findOne({ _id: id }) + await this.store.remove({ _id: id }) return getResponseData(result) } async list(appId) { - const result = await this.db.findAsync() + const result = await this.store.find() return getResponseData(result) } } diff --git a/mockServer/src/services/blockGroup.js b/mockServer/src/services/blockGroup.js index 8b4c0ae732..5e84590152 100644 --- a/mockServer/src/services/blockGroup.js +++ b/mockServer/src/services/blockGroup.js @@ -9,20 +9,15 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ -import DateStore from '@seald-io/nedb' -import { getDatabasePath, getResponseData } from '../tool/Common' +import createStore from '../store/StoreFactory' +import { getResponseData } from '../tool/Common' import appinfo from '../assets/json/appinfo.json' export default class BlockGroupService { constructor() { - this.db = new DateStore({ - filename: getDatabasePath('blockGroups.db'), - autoload: true - }) - - this.db.ensureIndex({ - fieldName: 'name', - unique: true + this.store = createStore('blockGroups', { + indexes: [{ fieldName: 'name', unique: true }], + namingFields: ['name'] }) this.blockGroupModel = { @@ -37,39 +32,39 @@ export default class BlockGroupService { async create(params) { const blockGroupData = { ...this.blockGroupModel, ...params } blockGroupData.app = appinfo.app - const result = await this.db.insertAsync(blockGroupData) + const result = await this.store.insert(blockGroupData) const { _id } = result - await this.db.updateAsync({ _id }, { $set: { id: _id } }) + await this.store.update({ _id }, { $set: { id: _id } }) result.id = result._id return getResponseData(result) } async update(id, params) { params.app = appinfo.app - await this.db.updateAsync({ _id: id }, { $set: params }) + await this.store.update({ _id: id }, { $set: params }) - const result = await this.db.findOneAsync({ _id: id }) + const result = await this.store.findOne({ _id: id }) return getResponseData(result) } async find(params) { if (params?.app || !params?.id) { - const result = await this.db.findAsync() + const result = await this.store.find() return getResponseData(result) } const { id } = params - const blockGroup = await this.db.findOneAsync({ _id: id }) + const blockGroup = await this.store.findOne({ _id: id }) return getResponseData([blockGroup]) } async delete(blockGroupId) { - const result = await this.db.findOneAsync({ _id: blockGroupId }) - await this.db.removeAsync({ _id: blockGroupId }) + const result = await this.store.findOne({ _id: blockGroupId }) + await this.store.remove({ _id: blockGroupId }) return getResponseData(result) } async list(appId) { - const result = await this.db.findAsync() + const result = await this.store.find() return getResponseData(result) } } diff --git a/mockServer/src/services/pages.js b/mockServer/src/services/pages.js index c1a71dcaad..572b3f4f4b 100644 --- a/mockServer/src/services/pages.js +++ b/mockServer/src/services/pages.js @@ -10,8 +10,32 @@ * */ -import DateStore from '@seald-io/nedb' -import { getDatabasePath, getResponseData } from '../tool/Common' +import createStore from '../store/StoreFactory' +import config from '../config/config' +import { getResponseData } from '../tool/Common' + +const formatPageContentForStorage = (pageContent) => { + if (pageContent === undefined) { + return pageContent + } + + if (config.dbMode === 'file') { + if (typeof pageContent === 'string') { + try { + return JSON.parse(pageContent) + } catch (e) { + return pageContent + } + } + return pageContent + } + + if (pageContent && typeof pageContent === 'object') { + return JSON.stringify(pageContent) + } + + return pageContent +} const parsePageContent = (item) => { if (item && item.page_content && typeof item.page_content === 'string') { @@ -26,21 +50,16 @@ const parsePageContent = (item) => { export default class PageService { constructor() { - this.db = new DateStore({ - filename: getDatabasePath('pages.db'), - autoload: true - }) - - this.db.ensureIndex({ - fieldName: '_id', - unique: true + this.store = createStore('pages', { + indexes: [{ fieldName: '_id', unique: true }], + namingFields: ['name'] }) this.userInfo = { id: 86, username: '开发者', - email: 'developer@lowcode.com', - confirmationToken: 'dfb2c162-351f-4f44-ad5f-8998', + email: 'demo@example.com', + confirmationToken: null, is_admin: true } @@ -60,8 +79,8 @@ export default class PageService { occupier: { id: 86, username: '开发者', - email: 'developer@lowcode.com', - confirmationToken: 'dfb2c162-351f-4f44-ad5f-8998', + email: 'demo@example.com', + confirmationToken: null, is_admin: true } } @@ -84,7 +103,7 @@ export default class PageService { pageData.route = pageData.name || 'Untitled' } - const existing = await this.db.findOneAsync({ + const existing = await this.store.findOne({ app: pageData.app.toString(), route: pageData.route }) @@ -97,30 +116,29 @@ export default class PageService { }) } - if (pageData.page_content && typeof pageData.page_content === 'object') { - pageData.page_content = JSON.stringify(pageData.page_content) - } + pageData.page_content = formatPageContentForStorage(pageData.page_content) - const result = await this.db.insertAsync(pageData) + const result = await this.store.insert(pageData) const { _id } = result - await this.db.updateAsync({ _id }, { $set: { id: _id } }) + await this.store.update({ _id }, { $set: { id: _id } }) result.id = result._id return getResponseData(parsePageContent(result)) } async update(id, params) { const updateData = { ...params } - if (updateData.page_content && typeof updateData.page_content === 'object') { - updateData.page_content = JSON.stringify(updateData.page_content) + + if (Object.prototype.hasOwnProperty.call(updateData, 'page_content')) { + updateData.page_content = formatPageContentForStorage(updateData.page_content) } - await this.db.updateAsync({ _id: id }, { $set: updateData }) - const result = await this.db.findOneAsync({ _id: id }) + await this.store.update({ _id: id }, { $set: updateData }) + const result = await this.store.findOne({ _id: id }) return getResponseData(parsePageContent(result)) } async list(appId) { - const result = await this.db.findAsync({ app: appId.toString() }) + const result = await this.store.find({ app: appId.toString() }) if (Array.isArray(result)) { result.forEach(parsePageContent) } @@ -128,15 +146,13 @@ export default class PageService { } async detail(pageId) { - const result = await this.db.findOneAsync({ _id: pageId }) - + const result = await this.store.findOne({ _id: pageId }) return getResponseData(parsePageContent(result)) } async delete(pageId) { - const result = await this.db.findOneAsync({ _id: pageId }) - - await this.db.removeAsync({ _id: pageId }) + const result = await this.store.findOne({ _id: pageId }) + await this.store.remove({ _id: pageId }) return getResponseData(parsePageContent(result)) } } diff --git a/mockServer/src/store/FileStore.js b/mockServer/src/store/FileStore.js new file mode 100644 index 0000000000..18aecc1fec --- /dev/null +++ b/mockServer/src/store/FileStore.js @@ -0,0 +1,434 @@ +/** + * 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. + * + */ + +const fs = require('fs') +const path = require('path') +const crypto = require('crypto') +const StoreAdapter = require('./StoreAdapter') + +/** + * FileStore - File-based storage adapter with atomic writes and concurrency support + */ +class FileStore extends StoreAdapter { + constructor(collectionName, dataPath, options = {}) { + super() + this.collectionName = collectionName + this.dataPath = dataPath + this.collectionPath = path.join(dataPath, collectionName) + this.namingFields = Array.isArray(options.namingFields) ? options.namingFields : [] + + // Extract unique fields from indexes array + this.uniqueFields = [] + if (options.indexes && Array.isArray(options.indexes)) { + this.uniqueFields = options.indexes.filter((idx) => idx.unique === true).map((idx) => idx.fieldName) + } + + // Ensure collection directory exists + this.ensureDirectory() + + // Detect whether the collection directory lives on a case-sensitive + // filesystem, so a file rename that only changes letter case (e.g. + // Login -> login) is handled correctly. On a case-insensitive FS (macOS + // APFS and Windows NTFS defaults) such a rename points at the same physical + // file, so writeAtomic + unlink would otherwise delete the just-written file. + this.caseSensitive = this.detectCaseSensitive() + } + + ensureDirectory() { + if (!fs.existsSync(this.dataPath)) { + fs.mkdirSync(this.dataPath, { recursive: true }) + } + if (!fs.existsSync(this.collectionPath)) { + fs.mkdirSync(this.collectionPath, { recursive: true }) + } + } + + /** + * Probe whether the collection directory lives on a case-sensitive filesystem + * by writing a marker file and checking whether a differently-cased name + * resolves to it. Returns true (case-sensitive) or false (case-insensitive), + * defaulting to false if probing fails so we never unlink on a case-only rename. + */ + detectCaseSensitive() { + const probeName = `.casesens-${crypto.randomBytes(4).toString('hex')}` + const probePath = path.join(this.collectionPath, probeName) + try { + fs.writeFileSync(probePath, '') + // On a case-insensitive FS the upper-cased name resolves to the same file. + return !fs.existsSync(path.join(this.collectionPath, probeName.toUpperCase())) + } catch { + // Conservative default: treat as case-insensitive to avoid data loss. + return false + } finally { + try { + if (fs.existsSync(probePath)) { + fs.unlinkSync(probePath) + } + } catch { + /* best-effort cleanup of the probe file */ + } + } + } + + /** + * Generate a unique ID similar to NeDB's format + */ + generateId() { + return crypto.randomBytes(8).toString('hex') + } + + sanitizeFileName(name) { + if (!name) { + return '' + } + + const normalized = String(name) + .trim() + .replace(/\s+/g, '-') + .replace(/[<>:"/\\|?*\u0000-\u001F]/g, '-') + .replace(/-+/g, '-') + .replace(/^\.+/, '') + .replace(/\.+$/, '') + .slice(0, 120) + + if (!normalized || normalized === '.' || normalized === '..') { + return '' + } + + return normalized + } + + buildFileBaseName(doc, allEntries = [], excludeId = null, reservedNames = null) { + let baseName = '' + + for (const field of this.namingFields) { + const value = this.sanitizeFileName(doc[field]) + if (value) { + baseName = value + break + } + } + + if (!baseName) { + baseName = this.sanitizeFileName(doc.id) || this.sanitizeFileName(doc._id) || 'record' + } + + const usedNames = new Set( + allEntries + .filter((entry) => entry.doc && entry.doc._id !== excludeId) + .map((entry) => path.parse(entry.fileName).name.toLowerCase()) + ) + if (reservedNames) { + for (const name of reservedNames) { + usedNames.add(name) + } + } + + if (!usedNames.has(baseName.toLowerCase())) { + return baseName + } + + const suffix = String(doc._id || doc.id || 'item').slice(0, 6) + let candidate = `${baseName}-${suffix}` + let seq = 2 + + while (usedNames.has(candidate.toLowerCase())) { + candidate = `${baseName}-${suffix}-${seq}` + seq += 1 + } + + return candidate + } + + /** + * Atomic write using temporary file and rename + */ + writeAtomic(filePath, data) { + const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}` + try { + fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf8') + fs.renameSync(tempPath, filePath) + } catch (error) { + // Clean up temp file if it exists + if (fs.existsSync(tempPath)) { + fs.unlinkSync(tempPath) + } + throw error + } + } + + readDocumentFromFilePath(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf8') + const doc = JSON.parse(content) + return doc + } catch (error) { + console.error(`Error reading document ${filePath}:`, error) + return null + } + } + + readAllEntries() { + if (!fs.existsSync(this.collectionPath)) { + return [] + } + + const files = fs.readdirSync(this.collectionPath) + const entries = [] + + for (const fileName of files) { + if (!fileName.endsWith('.json') || fileName.includes('.tmp.')) { + continue + } + + const filePath = path.join(this.collectionPath, fileName) + const doc = this.readDocumentFromFilePath(filePath) + if (doc) { + if (!doc._id && doc.id !== undefined) { + doc._id = String(doc.id) + } + entries.push({ doc, filePath, fileName }) + } + } + + return entries + } + + readAllDocuments() { + return this.readAllEntries().map((entry) => entry.doc) + } + + /** + * Deep equality, mirroring NeDB's areThingsEqual: primitives by ===, Dates by + * timestamp, arrays and plain objects by recursive value comparison. An array + * never equals a non-array. + */ + deepEqual(a, b) { + if (a === b) return true + if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false + + const aIsArray = Array.isArray(a) + const bIsArray = Array.isArray(b) + if (aIsArray !== bIsArray) return false + + if (a instanceof Date || b instanceof Date) { + return a instanceof Date && b instanceof Date && a.getTime() === b.getTime() + } + + const aKeys = Object.keys(a) + const bKeys = Object.keys(b) + if (aKeys.length !== bKeys.length) return false + for (const k of aKeys) { + if (!Object.prototype.hasOwnProperty.call(b, k)) return false + if (!this.deepEqual(a[k], b[k])) return false + } + return true + } + + /** + * Check if a document matches the query (NeDB-compatible semantics). + * An object query value is treated as an operator object only when every key + * starts with `$`; otherwise (plain object or array) it is deep-compared, so + * `{ config: { mode: 'x' } }` or `{ tags: ['a'] }` does not silently match all + * records. A RegExp query value is matched like `$regex`. + */ + matchesQuery(doc, query) { + if (!query || Object.keys(query).length === 0) { + return true + } + + for (const [key, value] of Object.entries(query)) { + if (value !== null && typeof value === 'object' && !(value instanceof RegExp)) { + const keys = Object.keys(value) + const isOperatorObject = keys.length > 0 && keys.every((k) => k.startsWith('$')) + + if (isOperatorObject) { + if (value.$regex && !value.$regex.test(doc[key])) return false + if (value.$ne !== undefined && doc[key] === value.$ne) return false + if (value.$in !== undefined && !value.$in.includes(doc[key])) return false + if (value.$nin !== undefined && value.$nin.includes(doc[key])) return false + if (value.$gt !== undefined && !(doc[key] > value.$gt)) return false + if (value.$gte !== undefined && !(doc[key] >= value.$gte)) return false + if (value.$lt !== undefined && !(doc[key] < value.$lt)) return false + if (value.$lte !== undefined && !(doc[key] <= value.$lte)) return false + } else if (!this.deepEqual(doc[key], value)) { + // Plain object or array query value: deep-compare (NeDB areThingsEqual) + return false + } + } else if (value instanceof RegExp) { + if (!value.test(doc[key])) return false + } else if (doc[key] !== value) { + // Simple equality check + return false + } + } + + return true + } + + /** + * Apply update operations to a document + */ + applyUpdate(doc, update) { + const newDoc = { ...doc } + + if (update.$set) { + Object.assign(newDoc, update.$set) + } + + if (update.$unset) { + for (const key of Object.keys(update.$unset)) { + delete newDoc[key] + } + } + + if (update.$inc) { + for (const [key, value] of Object.entries(update.$inc)) { + newDoc[key] = (newDoc[key] || 0) + value + } + } + + // If no operators, treat as direct replacement + if (!update.$set && !update.$unset && !update.$inc) { + Object.assign(newDoc, update) + } + + return newDoc + } + + /** + * Check for unique field violations + */ + async checkUniqueConstraints(data, excludeId = null) { + if (this.uniqueFields.length === 0) { + return + } + + const allDocs = this.readAllDocuments() + + for (const field of this.uniqueFields) { + if (data[field] !== undefined) { + const existing = allDocs.find((doc) => doc[field] === data[field] && doc._id !== excludeId) + if (existing) { + throw new Error(`Unique constraint violated for field: ${field}`) + } + } + } + } + + /** + * Insert a new document + */ + async insert(data) { + const id = data._id || this.generateId() + const doc = { + ...data, + _id: id + } + + // Check unique constraints + await this.checkUniqueConstraints(doc) + + const allEntries = this.readAllEntries() + const existing = allEntries.find((entry) => entry.doc && entry.doc._id === id) + if (existing) { + throw new Error(`Document with id ${id} already exists`) + } + + const fileBaseName = this.buildFileBaseName(doc, allEntries) + const filePath = path.join(this.collectionPath, `${fileBaseName}.json`) + + this.writeAtomic(filePath, doc) + return doc + } + + /** + * Update documents matching the query + */ + async update(query, update, options = {}) { + const allEntries = this.readAllEntries() + const matchingEntries = allEntries.filter((entry) => this.matchesQuery(entry.doc, query)) + + if (matchingEntries.length === 0) { + return 0 + } + + const multi = options.multi === true + const entriesToUpdate = multi ? matchingEntries : [matchingEntries[0]] + const reservedNames = new Set() + + for (const entry of entriesToUpdate) { + const updatedDoc = this.applyUpdate(entry.doc, update) + + // Check unique constraints for updated document + await this.checkUniqueConstraints(updatedDoc, entry.doc._id) + + const fileBaseName = this.buildFileBaseName(updatedDoc, allEntries, entry.doc._id, reservedNames) + const targetPath = path.join(this.collectionPath, `${fileBaseName}.json`) + reservedNames.add(fileBaseName.toLowerCase()) + + this.writeAtomic(targetPath, updatedDoc) + // Only unlink the previous file when it is a genuinely different physical + // file. On a case-insensitive FS a case-only rename (Login -> login) points + // at the same file, so unlinking would delete the document we just wrote. + const pathsDiffer = this.caseSensitive + ? targetPath !== entry.filePath + : targetPath.toLowerCase() !== entry.filePath.toLowerCase() + if (pathsDiffer && fs.existsSync(entry.filePath)) { + fs.unlinkSync(entry.filePath) + } + } + + return entriesToUpdate.length + } + + /** + * Find documents matching the query + */ + async find(query) { + const allDocs = this.readAllDocuments() + return allDocs.filter((doc) => this.matchesQuery(doc, query)) + } + + /** + * Find one document matching the query + */ + async findOne(query) { + const allDocs = this.readAllDocuments() + return allDocs.find((doc) => this.matchesQuery(doc, query)) || null + } + + /** + * Remove documents matching the query + */ + async remove(query, options = {}) { + const allEntries = this.readAllEntries() + const matchingEntries = allEntries.filter((entry) => this.matchesQuery(entry.doc, query)) + + if (matchingEntries.length === 0) { + return 0 + } + + const multi = options.multi === true + const entriesToRemove = multi ? matchingEntries : [matchingEntries[0]] + + for (const entry of entriesToRemove) { + if (fs.existsSync(entry.filePath)) { + fs.unlinkSync(entry.filePath) + } + } + + return entriesToRemove.length + } +} + +module.exports = FileStore diff --git a/mockServer/src/store/NedbStore.js b/mockServer/src/store/NedbStore.js new file mode 100644 index 0000000000..72785925a7 --- /dev/null +++ b/mockServer/src/store/NedbStore.js @@ -0,0 +1,67 @@ +/** + * 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. + * + */ + +const DateStore = require('@seald-io/nedb') +const StoreAdapter = require('./StoreAdapter') + +/** + * NeDB storage adapter implementation + * Wraps @seald-io/nedb to provide standard storage interface + */ +class NedbStore extends StoreAdapter { + constructor(options) { + super() + this.db = new DateStore({ + filename: options.filename, + autoload: true + }) + + // Process indexes array (matches NeDB format) + if (options.indexes && Array.isArray(options.indexes)) { + options.indexes.forEach((indexConfig) => { + this.db.ensureIndex(indexConfig) + }) + } + } + + async insert(data) { + const result = await this.db.insertAsync(data) + return result + } + + async update(query, update, options = {}) { + // Pass update operators directly and forward `multi` to NeDB. Default + // multi:false matches the StoreAdapter/FileStore contract (single record). + // Returns the count of affected records per the store contract. + const { numAffected } = await this.db.updateAsync(query, update, { multi: options.multi === true }) + return numAffected + } + + async find(query = {}) { + const result = await this.db.findAsync(query) + return result + } + + async findOne(query) { + const result = await this.db.findOneAsync(query) + return result + } + + async remove(query, options = {}) { + // Forward `multi` to NeDB; default multi:false matches the store contract. + // Returns the count of removed records. + const numRemoved = await this.db.removeAsync(query, { multi: options.multi === true }) + return numRemoved + } +} + +module.exports = NedbStore diff --git a/mockServer/src/store/StoreAdapter.js b/mockServer/src/store/StoreAdapter.js new file mode 100644 index 0000000000..e860af312c --- /dev/null +++ b/mockServer/src/store/StoreAdapter.js @@ -0,0 +1,73 @@ +/** + * 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. + * + */ + +/** + * Base class for storage adapters + * Defines the standard interface that all storage implementations must follow + */ +class StoreAdapter { + /** + * Insert a single record + * @param {Object} data - The data to insert + * @returns {Promise} The inserted record with generated ID + */ + async insert(data) { + throw new Error('insert() must be implemented by subclass') + } + + /** + * Update records matching the query + * @param {Object} query - Query criteria + * @param {Object} update - Update operations (e.g., { $set: {...} }) + * @returns {Promise} Number of records updated + */ + async update(query, update) { + throw new Error('update() must be implemented by subclass') + } + + /** + * Find multiple records matching the query + * @param {Object} query - Query criteria + * @returns {Promise} Array of matching records + */ + async find(query) { + throw new Error('find() must be implemented by subclass') + } + + /** + * Find a single record matching the query + * @param {Object} query - Query criteria + * @returns {Promise} The matching record or null + */ + async findOne(query) { + throw new Error('findOne() must be implemented by subclass') + } + + /** + * Remove records matching the query + * @param {Object} query - Query criteria + * @returns {Promise} Number of records removed + */ + async remove(query) { + throw new Error('remove() must be implemented by subclass') + } + + /** + * Ensure an index on a field (for unique constraints, etc.) + * @param {Object} options - Index options (e.g., { fieldName: 'route', unique: true }) + */ + ensureIndex(options) { + // Optional: some stores may not need this + } +} + +module.exports = StoreAdapter diff --git a/mockServer/src/store/StoreFactory.js b/mockServer/src/store/StoreFactory.js new file mode 100644 index 0000000000..52f29064d1 --- /dev/null +++ b/mockServer/src/store/StoreFactory.js @@ -0,0 +1,40 @@ +/** + * 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. + * + */ + +const config = require('../config/config') +const { getDatabasePath } = require('../tool/Common') +const NedbStore = require('./NedbStore') +const FileStore = require('./FileStore') + +/** + * Factory function to create the appropriate store instance based on configuration + * @param {string} collectionName - Name of the collection (e.g., 'pages', 'apps') + * @param {Object} options - Additional options for the store + * @param {Array} options.indexes - Index configurations (e.g., [{ fieldName: 'route', unique: true }]) + * @returns {StoreAdapter} Store instance + */ +function createStore(collectionName, options = {}) { + const dbMode = config.dbMode || 'db' + + if (dbMode === 'file') { + // Pass dataPath from config + return new FileStore(collectionName, config.fileDbPath, options) + } else { + // Build NeDB options with filename + const nedbOptions = { + filename: getDatabasePath(`${collectionName}.db`), + ...options + } + return new NedbStore(nedbOptions) + } +} + +module.exports = createStore diff --git a/mockServer/test/store/adapter.contract.test.js b/mockServer/test/store/adapter.contract.test.js new file mode 100644 index 0000000000..66d29ef62f --- /dev/null +++ b/mockServer/test/store/adapter.contract.test.js @@ -0,0 +1,171 @@ +/** + * Adapter contract tests — run the SAME assertions against BOTH NedbStore and + * FileStore to lock in behavioural parity. These directly cover: + * - default `multi:false` for update/remove (matches NeDB semantics) + * - update/remove return a number (the StoreAdapter contract) + * - compound query operators evaluated independently ({ $gte, $lte }, ...) + */ + +const fs = require('fs') +const os = require('os') +const path = require('path') +const NedbStore = require('../../src/store/NedbStore') +const FileStore = require('../../src/store/FileStore') + +const INDEX_OPTS = { indexes: [{ fieldName: '_id', unique: true }] } + +// Each adapter is constructed fresh in beforeEach for isolation. Both are built +// in a per-test temp directory so nothing touches the real mockServer/data or +// src/database files, and no MOCK_DB_MODE env is required. +const createdDirs = [] +const adapterFactories = [ + [ + 'NedbStore', + () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'contract-nedb-')) + createdDirs.push(dir) + return new NedbStore({ filename: path.join(dir, 'contract.db'), ...INDEX_OPTS }) + } + ], + [ + 'FileStore', + () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'contract-file-')) + createdDirs.push(dir) + return new FileStore('things', dir, { ...INDEX_OPTS, namingFields: ['name'] }) + } + ] +] + +afterAll(() => { + for (const dir of createdDirs) { + try { + fs.rmSync(dir, { recursive: true, force: true }) + } catch { + /* best-effort cleanup */ + } + } +}) + +describe.each(adapterFactories)('%s honours the StoreAdapter contract', (_name, factory) => { + let store + + beforeEach(() => { + store = factory() + }) + + const seedGroup = async () => { + await store.insert({ _id: 'a', name: 'alpha', group: 'g' }) + await store.insert({ _id: 'b', name: 'beta', group: 'g' }) + await store.insert({ _id: 'c', name: 'gamma', group: 'g' }) + } + + test('insert returns the document with its _id', async () => { + const doc = await store.insert({ _id: 'a', name: 'alpha' }) + expect(doc._id).toBe('a') + }) + + test('findOne / find read back inserted docs', async () => { + await store.insert({ _id: 'a', name: 'alpha' }) + const one = await store.findOne({ _id: 'a' }) + expect(one && one.name).toBe('alpha') + const all = await store.find({}) + expect(all).toHaveLength(1) + }) + + test('unique _id constraint rejects a duplicate insert', async () => { + await store.insert({ _id: 'a', name: 'alpha' }) + await expect(store.insert({ _id: 'a', name: 'alpha-2' })).rejects.toBeDefined() + }) + + test('update defaults to a single record and returns a number count', async () => { + await seedGroup() + const affected = await store.update({ group: 'g' }, { $set: { tagged: true } }) + expect(affected).toBe(1) + expect(typeof affected).toBe('number') + const tagged = await store.find({ tagged: true }) + expect(tagged).toHaveLength(1) + }) + + test('update with { multi: true } updates all matches and returns the count', async () => { + await seedGroup() + const affected = await store.update({ group: 'g' }, { $set: { tagged: true } }, { multi: true }) + expect(affected).toBe(3) + const tagged = await store.find({ tagged: true }) + expect(tagged).toHaveLength(3) + }) + + test('remove defaults to a single record and returns a number count', async () => { + await seedGroup() + const removed = await store.remove({ group: 'g' }) + expect(removed).toBe(1) + expect(typeof removed).toBe('number') + const remaining = await store.find({ group: 'g' }) + expect(remaining).toHaveLength(2) + }) + + test('remove with { multi: true } removes all matches', async () => { + await seedGroup() + const removed = await store.remove({ group: 'g' }, { multi: true }) + expect(removed).toBe(3) + const remaining = await store.find({ group: 'g' }) + expect(remaining).toHaveLength(0) + }) + + test('compound query { $gte, $lte } evaluates every operator on a field', async () => { + await store.insert({ _id: 'p1', age: 10 }) + await store.insert({ _id: 'p2', age: 20 }) + await store.insert({ _id: 'p3', age: 40 }) + const matches = await store.find({ age: { $gte: 15, $lte: 30 } }) + expect(matches).toHaveLength(1) + expect(matches[0]._id).toBe('p2') + }) + + test('single operators $ne / $in / $nin / $regex behave consistently', async () => { + await store.insert({ _id: 'p1', name: 'foo' }) + await store.insert({ _id: 'p2', name: 'bar' }) + await store.insert({ _id: 'p3', name: 'baz' }) + + expect(await store.find({ _id: { $ne: 'p1' } })).toHaveLength(2) + expect(await store.find({ _id: { $in: ['p1', 'p2'] } })).toHaveLength(2) + expect(await store.find({ _id: { $nin: ['p1'] } })).toHaveLength(2) + expect(await store.find({ name: { $regex: /^ba/ } })).toHaveLength(2) + }) + + test('plain object query value deep-compares instead of matching all', async () => { + await store.insert({ _id: 'o1', config: { mode: 'x', n: 1 } }) + await store.insert({ _id: 'o2', config: { mode: 'y', n: 2 } }) + await store.insert({ _id: 'o3', config: { mode: 'x', n: 3 } }) + + const matches = await store.find({ config: { mode: 'x', n: 1 } }) + expect(matches).toHaveLength(1) + expect(matches[0]._id).toBe('o1') + + // a plain object that matches no record must return none — not everything + expect(await store.find({ config: { mode: 'z' } })).toHaveLength(0) + }) + + test('array query value deep-compares instead of matching all', async () => { + await store.insert({ _id: 'a1', tags: ['red', 'blue'] }) + await store.insert({ _id: 'a2', tags: ['green'] }) + + const matches = await store.find({ tags: ['red', 'blue'] }) + expect(matches).toHaveLength(1) + expect(matches[0]._id).toBe('a1') + + expect(await store.find({ tags: ['purple'] })).toHaveLength(0) + }) + + test('updating a naming field to a case-only variant keeps the document', async () => { + await store.insert({ _id: 'login', name: 'Login' }) + const affected = await store.update({ _id: 'login' }, { $set: { name: 'login' } }) + expect(affected).toBe(1) + // The document must still exist and read back with the updated value. + // Regression guard: on a case-insensitive filesystem (macOS/Windows default) + // a case-only rename used to make FileStore unlink the just-written file. + const doc = await store.findOne({ _id: 'login' }) + expect(doc).not.toBeNull() + expect(doc.name).toBe('login') + expect(await store.find({})).toHaveLength(1) + }) +}) diff --git a/mockServer/test/store/apps.rollback.test.js b/mockServer/test/store/apps.rollback.test.js new file mode 100644 index 0000000000..e11f7085d5 --- /dev/null +++ b/mockServer/test/store/apps.rollback.test.js @@ -0,0 +1,66 @@ +/** + * AppsService.create() best-effort rollback test. + * + * create() awaits the app insert then the schema insert; if the schema insert + * fails it must remove the just-inserted app (they live in different collections) + * and rethrow. We force file mode at a temp dir so nothing touches real data, + * then swap in a throwing schemaStore and assert the app is rolled back. + * + * Uses jest.isolateModules so config/StoreFactory are loaded fresh with the env + * we set, regardless of any module caching from other test files. + */ + +const fs = require('fs') +const os = require('os') +const path = require('path') + +describe('AppsService.create() rollback', () => { + let tmpDir + let prevMode + let prevPath + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'apps-rollback-')) + prevMode = process.env.MOCK_DB_MODE + prevPath = process.env.MOCK_FILE_DB_PATH + process.env.MOCK_DB_MODE = 'file' + process.env.MOCK_FILE_DB_PATH = tmpDir + }) + + afterEach(() => { + if (prevMode === undefined) delete process.env.MOCK_DB_MODE + else process.env.MOCK_DB_MODE = prevMode + if (prevPath === undefined) delete process.env.MOCK_FILE_DB_PATH + else process.env.MOCK_FILE_DB_PATH = prevPath + try { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } catch { + /* best-effort cleanup */ + } + }) + + test('removes the inserted app and rethrows when the schema insert fails', async () => { + let AppsService + jest.isolateModules(() => { + AppsService = require('../../src/services/apps').default + }) + + const service = new AppsService() + // appList is empty in a fresh service, so the generated id falls back to 3. + const generatedId = 3 + + // Real FileStore for apps; throw on schema insert. + service.schemaStore = { + insert: jest.fn().mockRejectedValue(new Error('schema write failed')) + } + const removeSpy = jest.spyOn(service.store, 'remove') + + await expect(service.create({ name: 'rollback-app' })).rejects.toThrow('schema write failed') + + expect(service.schemaStore.insert).toHaveBeenCalledTimes(1) + expect(removeSpy).toHaveBeenCalledWith({ id: generatedId }) + + const leftover = await service.store.findOne({ id: generatedId }) + expect(leftover).toBeNull() + }) +}) diff --git a/package.json b/package.json index edd21d6ca4..618b80e507 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,18 @@ "scripts": { "preinstall": "npx only-allow pnpm", "dev": "pnpm run setup && concurrently 'pnpm:serve:backend' 'pnpm:serve:frontend'", + "dev:file": "pnpm run setup && concurrently 'pnpm:serve:backend:file' 'pnpm:serve:frontend'", "dev:withAuth": "pnpm run setup && pnpm --filter designer-demo dev:withAuth", "serve:frontend": "pnpm --filter designer-demo dev", "serve:backend": "pnpm --filter @opentiny/tiny-engine-mock dev", + "serve:backend:file": "pnpm --filter @opentiny/tiny-engine-mock dev:file", "build:plugin": "pnpm --filter @opentiny/tiny-engine-* --filter @opentiny/tiny-engine build", "build:alpha": "pnpm --filter designer-demo build:alpha", "build:prod": "pnpm --filter designer-demo build", "buildComponentSchemas": "node scripts/buildComponentSchemas.js", "lint": "eslint . --ext .js,.mjs,.jsx,.ts,.mts,.tsx,.vue --fix", "format": "prettier --write --list-different **/*{.vue,.js,.mjs,.jsx,.ts,.mts,.tsx,.html,.json}", - "prepare": "node -e \"if(require('fs').existsSync('.git')){process.exit(1)}\" || husky install", + "prepare": "node -e \"if(require('fs').existsSync('.git')){process.exit(1)}\" || husky install && node scripts/link-skills.js", "pub:premajor": "pnpm run build:plugin && pnpm run build:alpha && pnpm lerna version premajor --preid beta --no-push --yes && lerna publish from-package --pre-dist-tag beta --yes", "pub:preminor": "pnpm run build:plugin && pnpm run build:alpha && pnpm lerna version preminor --preid beta --no-push --yes && lerna publish from-package --pre-dist-tag beta --yes", "pub:prepatch": "pnpm run build:plugin && pnpm run build:alpha && pnpm lerna version prepatch --preid beta --no-push --yes && lerna publish from-package --pre-dist-tag beta --yes", diff --git a/scripts/link-skills.js b/scripts/link-skills.js new file mode 100644 index 0000000000..78e02a2128 --- /dev/null +++ b/scripts/link-skills.js @@ -0,0 +1,63 @@ +const fs = require('fs') +const path = require('path') + +// Canonical skills live in .agents/skills (shared across agent tools). +// Claude Code only reads .claude/skills, so we expose the same files via a +// link: a relative symlink on macOS/Linux, a junction on Windows (junctions +// need no admin rights / developer mode, unlike real symbolic links). +// Runs automatically through the root `prepare` script on every `pnpm install`; +// can also be invoked directly: `node scripts/link-skills.js`. + +const repoRoot = path.resolve(__dirname, '..') +const source = path.join(repoRoot, '.agents', 'skills') +const link = path.join(repoRoot, '.claude', 'skills') + +function pathExists(p) { + try { + fs.lstatSync(p) + return true + } catch { + return false + } +} + +function createLink() { + if (!fs.existsSync(source)) { + console.warn(`[link-skills] source missing, skipping: ${source}`) + return + } + + fs.mkdirSync(path.dirname(link), { recursive: true }) + + // Remove any existing link/junction so re-runs are idempotent. + if (pathExists(link)) { + const stat = fs.lstatSync(link) + if (stat.isSymbolicLink()) { + fs.rmSync(link) + } else if (stat.isDirectory()) { + if (fs.readdirSync(link).length !== 0) { + console.warn(`[link-skills] ${link} is a non-empty directory, leaving it untouched`) + return + } + fs.rmSync(link, { recursive: true }) + } else { + fs.rmSync(link) + } + } + + if (process.platform === 'win32') { + // Junction: absolute target, no admin rights required. + fs.symlinkSync(source, link, 'junction') + } else { + // Relative symlink so the repo stays relocatable. + fs.symlinkSync(path.relative(path.dirname(link), source), link) + } + console.log('[link-skills] linked .claude/skills -> .agents/skills') +} + +try { + createLink() +} catch (err) { + // Never fail the install: skills simply won't be visible to Claude Code. + console.warn(`[link-skills] could not create link: ${err.message}`) +}