formforgetest 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # formforgetest
2
+
3
+ FormForge **表单渲染引擎**(Vue 2.7)。解析 `pageSchema` JSON,递归渲染 LcInput / LcButton / LcSelect / LcContainer 等物料。
4
+
5
+ **不包含**编辑器(画布、属性面板、拖拽)。
6
+
7
+ npm 包名 **`formforgetest`**(因 `@formforge` 已被占用,本书案例使用此名称发布)。
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ # 本地 monorepo(file 协议)
13
+ npm install file:../pagecraft/packages/formforge-runtime
14
+
15
+ # 或从 npm 安装
16
+ npm install formforgetest
17
+ ```
18
+
19
+ ## 发布到 npm
20
+
21
+ 完整步骤见 **[PUBLISH.md](./PUBLISH.md)**(`npm login` → `npm publish` → 业务项目 `npm install formforgetest`)。
22
+
23
+ ## 使用
24
+
25
+ ```js
26
+ // main.js
27
+ import Vue from 'vue'
28
+ import FormForgeRuntime from 'formforgetest'
29
+
30
+ Vue.use(FormForgeRuntime)
31
+ ```
32
+
33
+ ```vue
34
+ <!-- App.vue -->
35
+ <template>
36
+ <FormForgePage :schema="pageSchema" />
37
+ </template>
38
+
39
+ <script>
40
+ import pageSchema from './schema.json'
41
+
42
+ export default {
43
+ data() {
44
+ return { pageSchema }
45
+ }
46
+ }
47
+ </script>
48
+ ```
49
+
50
+ 也可按需引入:
51
+
52
+ ```js
53
+ import { SchemaRenderer, PageNodeStack, EDITOR_MODE } from 'formforgetest'
54
+ ```
55
+
56
+ ## pageSchema 结构
57
+
58
+ ```json
59
+ {
60
+ "title": "登录页",
61
+ "nodes": [
62
+ {
63
+ "id": "n1",
64
+ "type": "input",
65
+ "props": { "label": "用户名", "fieldKey": "username" }
66
+ }
67
+ ]
68
+ }
69
+ ```
70
+
71
+ 详见 `examples/formforge-consumer` 模拟业务项目。
package/index.js ADDED
@@ -0,0 +1,45 @@
1
+ import SchemaRenderer from './src/components/business/SchemaRenderer.vue'
2
+ import PageNodeStack from './src/components/business/PageNodeStack.vue'
3
+ import FormForgePage from './src/components/business/FormForgePage.vue'
4
+ import LcButton from './src/components/base/LcButton.vue'
5
+ import LcInput from './src/components/base/LcInput.vue'
6
+ import LcContainer from './src/components/base/LcContainer.vue'
7
+ import LcSelect from './src/components/base/LcSelect.vue'
8
+ import EmptyFallback from './src/components/base/EmptyFallback.vue'
9
+
10
+ export { EDITOR_MODE } from './src/constants/editor'
11
+ export * from './src/utils/props'
12
+ export * from './src/widgets'
13
+
14
+ export {
15
+ SchemaRenderer,
16
+ PageNodeStack,
17
+ FormForgePage,
18
+ LcButton,
19
+ LcInput,
20
+ LcContainer,
21
+ LcSelect,
22
+ EmptyFallback
23
+ }
24
+
25
+ const pluginComponents = [
26
+ SchemaRenderer,
27
+ PageNodeStack,
28
+ FormForgePage,
29
+ LcButton,
30
+ LcInput,
31
+ LcContainer,
32
+ LcSelect,
33
+ EmptyFallback
34
+ ]
35
+
36
+ /** Vue.use(FormForgeRuntime) — 全局注册渲染引擎组件 */
37
+ export default {
38
+ install(Vue) {
39
+ pluginComponents.forEach(comp => {
40
+ if (comp.name) {
41
+ Vue.component(comp.name, comp)
42
+ }
43
+ })
44
+ }
45
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "formforgetest",
3
+ "version": "0.1.0",
4
+ "description": "FormForge 表单渲染引擎 — SchemaRenderer + widget 物料(Vue 2.7)",
5
+ "main": "index.js",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "vue2",
9
+ "form",
10
+ "low-code",
11
+ "schema",
12
+ "formforge",
13
+ "renderer"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/your-org/vue2-book.git",
18
+ "directory": "examples/pagecraft/packages/formforge-runtime"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "peerDependencies": {
24
+ "vue": "^2.7.0"
25
+ },
26
+ "files": [
27
+ "index.js",
28
+ "src",
29
+ "README.md"
30
+ ]
31
+ }
@@ -0,0 +1,27 @@
1
+ <script>
2
+ export default {
3
+ name: 'EmptyFallback',
4
+ functional: true,
5
+ props: {
6
+ type: { type: String, default: '' }
7
+ },
8
+ render(h, { props }) {
9
+ return h(
10
+ 'div',
11
+ { class: 'lc-fallback' },
12
+ `未知物料:${props.type || '?'}`
13
+ )
14
+ }
15
+ }
16
+ </script>
17
+
18
+ <style>
19
+ .lc-fallback {
20
+ padding: 8px 12px;
21
+ font-size: 12px;
22
+ color: #999;
23
+ background: #fff5f5;
24
+ border: 1px dashed #e57373;
25
+ border-radius: 4px;
26
+ }
27
+ </style>
@@ -0,0 +1,120 @@
1
+ <template>
2
+ <button
3
+ class="lc-btn"
4
+ :class="'lc-btn--' + type"
5
+ v-bind="$attrs"
6
+ :disabled="disabled || loading"
7
+ @click="handleClick"
8
+ >
9
+ <slot>{{ loading ? '请求中…' : label }}</slot>
10
+ </button>
11
+ </template>
12
+
13
+ <script>
14
+ import { EDITOR_MODE } from '../../constants/editor'
15
+ import { isApiBinding, isButtonVariant } from '../../utils/props'
16
+
17
+ export default {
18
+ name: 'LcButton',
19
+ inheritAttrs: false,
20
+ inject: {
21
+ editorContext: { default: () => ({ mode: EDITOR_MODE.PREVIEW }) },
22
+ formContext: { default: null }
23
+ },
24
+ props: {
25
+ label: { type: String, default: '按钮' },
26
+ type: {
27
+ type: String,
28
+ default: 'primary',
29
+ validator: isButtonVariant
30
+ },
31
+ disabled: { type: Boolean, default: false },
32
+ apiBinding: {
33
+ type: Object,
34
+ default: () => ({ enabled: false, url: '', method: 'POST', params: [] }),
35
+ validator: isApiBinding
36
+ }
37
+ },
38
+ data() {
39
+ return {
40
+ loading: false
41
+ }
42
+ },
43
+ methods: {
44
+ handleClick(event) {
45
+ this.$emit('click', event)
46
+ if (this.editorContext.mode === EDITOR_MODE.EDIT) {
47
+ return
48
+ }
49
+ if (this.apiBinding.enabled && this.apiBinding.url) {
50
+ this.invokeApi()
51
+ }
52
+ },
53
+ buildRequestPayload() {
54
+ const payload = {}
55
+ ;(this.apiBinding.params || []).forEach(item => {
56
+ if (!item.name) return
57
+ if (item.source === 'field') {
58
+ payload[item.name] = this.formContext?.getValue(item.fieldKey) ?? ''
59
+ } else {
60
+ payload[item.name] = item.value
61
+ }
62
+ })
63
+ return payload
64
+ },
65
+ async invokeApi() {
66
+ const { url, method } = this.apiBinding
67
+ const payload = this.buildRequestPayload()
68
+ this.loading = true
69
+ try {
70
+ let requestUrl = url
71
+ const options = { method }
72
+ if (method === 'GET') {
73
+ const qs = new URLSearchParams(payload).toString()
74
+ if (qs) {
75
+ requestUrl += (url.includes('?') ? '&' : '?') + qs
76
+ }
77
+ } else {
78
+ options.headers = { 'Content-Type': 'application/json' }
79
+ options.body = JSON.stringify(payload)
80
+ }
81
+ const response = await fetch(requestUrl, options)
82
+ if (!response.ok) {
83
+ throw new Error(`HTTP ${response.status}`)
84
+ }
85
+ const data = await response.json()
86
+ this.$emit('api-success', data)
87
+ window.alert(`接口请求成功:${JSON.stringify(data).slice(0, 160)}…`)
88
+ } catch (error) {
89
+ this.$emit('api-error', error)
90
+ window.alert(`接口请求失败:${error.message}`)
91
+ } finally {
92
+ this.loading = false
93
+ }
94
+ }
95
+ }
96
+ }
97
+ </script>
98
+
99
+ <style scoped>
100
+ .lc-btn {
101
+ padding: 8px 16px;
102
+ border: none;
103
+ border-radius: 4px;
104
+ cursor: pointer;
105
+ font-size: 14px;
106
+ font-family: inherit;
107
+ }
108
+ .lc-btn--primary {
109
+ background: #41b883;
110
+ color: #fff;
111
+ }
112
+ .lc-btn--default {
113
+ background: #f0f0f0;
114
+ color: #333;
115
+ }
116
+ .lc-btn:disabled {
117
+ opacity: 0.5;
118
+ cursor: not-allowed;
119
+ }
120
+ </style>
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <div class="lc-grid" :style="gridStyle">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script>
8
+ export default {
9
+ name: 'LcContainer',
10
+ props: {
11
+ columns: {
12
+ type: Number,
13
+ default: 2,
14
+ validator: v => v >= 1 && v <= 4
15
+ },
16
+ gap: { type: Number, default: 12 },
17
+ padding: { type: Number, default: 12 }
18
+ },
19
+ computed: {
20
+ gridStyle() {
21
+ return {
22
+ padding: `${this.padding}px`,
23
+ gap: `${this.gap}px`,
24
+ gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))`
25
+ }
26
+ }
27
+ }
28
+ }
29
+ </script>
30
+
31
+ <style scoped>
32
+ .lc-grid {
33
+ display: grid;
34
+ width: 100%;
35
+ border: 1px dashed #ccc;
36
+ border-radius: 4px;
37
+ min-height: 56px;
38
+ background: #fafafa;
39
+ box-sizing: border-box;
40
+ }
41
+ </style>
@@ -0,0 +1,105 @@
1
+ <template>
2
+ <div class="lc-input-wrap">
3
+ <label v-if="label" class="lc-input__label" :for="inputId">{{ label }}</label>
4
+ <input
5
+ :id="inputId"
6
+ class="lc-input"
7
+ :value="innerValue"
8
+ :placeholder="placeholder"
9
+ :disabled="disabled"
10
+ :name="fieldKey || undefined"
11
+ v-bind="$attrs"
12
+ @input="onInput"
13
+ />
14
+ </div>
15
+ </template>
16
+
17
+ <script>
18
+ import { EDITOR_MODE } from '../../constants/editor'
19
+
20
+ let inputSeed = 0
21
+
22
+ export default {
23
+ name: 'LcInput',
24
+ inheritAttrs: false,
25
+ inject: {
26
+ editorContext: { default: () => ({ mode: EDITOR_MODE.PREVIEW }) },
27
+ formContext: { default: null }
28
+ },
29
+ props: {
30
+ label: { type: String, default: '' },
31
+ fieldKey: { type: String, default: '' },
32
+ value: { type: String, default: '' },
33
+ placeholder: { type: String, default: '' },
34
+ disabled: { type: Boolean, default: false }
35
+ },
36
+ data() {
37
+ return {
38
+ inputId: `lc-input-${++inputSeed}`,
39
+ localValue: this.value
40
+ }
41
+ },
42
+ computed: {
43
+ innerValue() {
44
+ if (this.formContext && this.fieldKey) {
45
+ return this.formContext.getValue(this.fieldKey)
46
+ }
47
+ return this.localValue
48
+ }
49
+ },
50
+ watch: {
51
+ value(next) {
52
+ this.localValue = next
53
+ this.syncFormContext(next)
54
+ },
55
+ fieldKey(next, prev) {
56
+ if (prev && this.formContext) {
57
+ this.formContext.setValue(prev, '')
58
+ }
59
+ this.syncFormContext(this.innerValue)
60
+ }
61
+ },
62
+ mounted() {
63
+ this.syncFormContext(this.value)
64
+ },
65
+ methods: {
66
+ syncFormContext(val) {
67
+ if (this.formContext && this.fieldKey) {
68
+ this.formContext.setValue(this.fieldKey, val)
69
+ }
70
+ },
71
+ onInput(event) {
72
+ const val = event.target.value
73
+ this.localValue = val
74
+ this.$emit('input', val)
75
+ this.syncFormContext(val)
76
+ }
77
+ }
78
+ }
79
+ </script>
80
+
81
+ <style scoped>
82
+ .lc-input-wrap {
83
+ width: 100%;
84
+ }
85
+ .lc-input__label {
86
+ display: block;
87
+ margin-bottom: 6px;
88
+ font-size: 13px;
89
+ font-weight: 500;
90
+ color: #333;
91
+ }
92
+ .lc-input {
93
+ width: 100%;
94
+ padding: 8px 12px;
95
+ border: 1px solid #ddd;
96
+ border-radius: 4px;
97
+ font-size: 14px;
98
+ font-family: inherit;
99
+ box-sizing: border-box;
100
+ }
101
+ .lc-input:focus {
102
+ outline: none;
103
+ border-color: #41b883;
104
+ }
105
+ </style>
@@ -0,0 +1,124 @@
1
+ <template>
2
+ <div class="lc-select-wrap">
3
+ <label v-if="label" class="lc-select__label" :for="selectId">{{ label }}</label>
4
+ <select
5
+ :id="selectId"
6
+ class="lc-select"
7
+ :value="innerValue"
8
+ :disabled="disabled"
9
+ :name="fieldKey || undefined"
10
+ v-bind="$attrs"
11
+ @change="onChange"
12
+ >
13
+ <option v-if="placeholder" value="" disabled hidden>{{ placeholder }}</option>
14
+ <option
15
+ v-for="(opt, idx) in options"
16
+ :key="opt.value + '-' + idx"
17
+ :value="opt.value"
18
+ >
19
+ {{ opt.label }}
20
+ </option>
21
+ </select>
22
+ </div>
23
+ </template>
24
+
25
+ <script>
26
+ import { EDITOR_MODE } from '../../constants/editor'
27
+ import { isSelectOptions } from '../../utils/props'
28
+
29
+ let selectSeed = 0
30
+
31
+ export default {
32
+ name: 'LcSelect',
33
+ inheritAttrs: false,
34
+ inject: {
35
+ editorContext: { default: () => ({ mode: EDITOR_MODE.PREVIEW }) },
36
+ formContext: { default: null }
37
+ },
38
+ props: {
39
+ label: { type: String, default: '' },
40
+ fieldKey: { type: String, default: '' },
41
+ value: { type: String, default: '' },
42
+ placeholder: { type: String, default: '请选择' },
43
+ disabled: { type: Boolean, default: false },
44
+ options: {
45
+ type: Array,
46
+ default: () => [],
47
+ validator: isSelectOptions
48
+ }
49
+ },
50
+ data() {
51
+ return {
52
+ selectId: `lc-select-${++selectSeed}`,
53
+ localValue: this.value
54
+ }
55
+ },
56
+ computed: {
57
+ innerValue() {
58
+ if (this.formContext && this.fieldKey) {
59
+ return this.formContext.getValue(this.fieldKey)
60
+ }
61
+ return this.localValue
62
+ }
63
+ },
64
+ watch: {
65
+ value(next) {
66
+ this.localValue = next
67
+ this.syncFormContext(next)
68
+ },
69
+ fieldKey(next, prev) {
70
+ if (prev && this.formContext) {
71
+ this.formContext.setValue(prev, '')
72
+ }
73
+ this.syncFormContext(this.innerValue)
74
+ }
75
+ },
76
+ mounted() {
77
+ this.syncFormContext(this.value)
78
+ },
79
+ methods: {
80
+ syncFormContext(val) {
81
+ if (this.formContext && this.fieldKey) {
82
+ this.formContext.setValue(this.fieldKey, val)
83
+ }
84
+ },
85
+ onChange(event) {
86
+ const val = event.target.value
87
+ this.localValue = val
88
+ this.$emit('input', val)
89
+ this.syncFormContext(val)
90
+ }
91
+ }
92
+ }
93
+ </script>
94
+
95
+ <style scoped>
96
+ .lc-select-wrap {
97
+ width: 100%;
98
+ }
99
+ .lc-select__label {
100
+ display: block;
101
+ margin-bottom: 6px;
102
+ font-size: 13px;
103
+ font-weight: 500;
104
+ color: #333;
105
+ }
106
+ .lc-select {
107
+ width: 100%;
108
+ padding: 8px 12px;
109
+ border: 1px solid #ddd;
110
+ border-radius: 4px;
111
+ font-size: 14px;
112
+ font-family: inherit;
113
+ box-sizing: border-box;
114
+ background: #fff;
115
+ }
116
+ .lc-select:focus {
117
+ outline: none;
118
+ border-color: #41b883;
119
+ }
120
+ .lc-select:disabled {
121
+ opacity: 0.6;
122
+ cursor: not-allowed;
123
+ }
124
+ </style>
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <div class="formforge-page">
3
+ <p v-if="!schema.nodes || !schema.nodes.length" class="formforge-page__empty">
4
+ {{ emptyText }}
5
+ </p>
6
+ <PageNodeStack v-else>
7
+ <SchemaRenderer
8
+ v-for="node in schema.nodes"
9
+ :key="node.id"
10
+ :node="node"
11
+ />
12
+ </PageNodeStack>
13
+ </div>
14
+ </template>
15
+
16
+ <script>
17
+ import { EDITOR_MODE } from '../../constants/editor'
18
+ import SchemaRenderer from './SchemaRenderer.vue'
19
+ import PageNodeStack from './PageNodeStack.vue'
20
+
21
+ /**
22
+ * 开箱即用:传入 pageSchema JSON 即可渲染表单页。
23
+ * 业务项目 Vue.use(formforgetest) 后可直接 <FormForgePage :schema="..." />。
24
+ */
25
+ export default {
26
+ name: 'FormForgePage',
27
+ components: { SchemaRenderer, PageNodeStack },
28
+ props: {
29
+ schema: {
30
+ type: Object,
31
+ required: true,
32
+ validator(s) {
33
+ return s && Array.isArray(s.nodes)
34
+ }
35
+ },
36
+ emptyText: {
37
+ type: String,
38
+ default: '暂无表单节点'
39
+ }
40
+ },
41
+ data() {
42
+ return {
43
+ formValues: {}
44
+ }
45
+ },
46
+ provide() {
47
+ return {
48
+ editorContext: { mode: EDITOR_MODE.PREVIEW },
49
+ formContext: {
50
+ getValue: key => this.formValues[key] ?? '',
51
+ setValue: (key, val) => {
52
+ this.$set(this.formValues, key, val)
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ </script>
59
+
60
+ <style scoped>
61
+ .formforge-page__empty {
62
+ text-align: center;
63
+ color: #999;
64
+ padding: 32px 16px;
65
+ font-size: 14px;
66
+ }
67
+ </style>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <div class="lc-page-stack">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script>
8
+ export default {
9
+ name: 'PageNodeStack'
10
+ }
11
+ </script>
12
+
13
+ <style scoped>
14
+ .lc-page-stack {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: 12px;
18
+ }
19
+ </style>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <component
3
+ :is="componentType"
4
+ v-bind="boundProps"
5
+ >
6
+ <SchemaRenderer
7
+ v-for="child in node.children || []"
8
+ :key="child.id"
9
+ :node="child"
10
+ />
11
+ </component>
12
+ </template>
13
+
14
+ <script>
15
+ import { resolveWidgetSync } from '../../widgets'
16
+ import EmptyFallback from '../base/EmptyFallback.vue'
17
+
18
+ export default {
19
+ name: 'SchemaRenderer',
20
+ components: { EmptyFallback },
21
+ props: {
22
+ node: {
23
+ type: Object,
24
+ required: true,
25
+ validator(node) {
26
+ return node && typeof node.type === 'string' && node.props != null
27
+ }
28
+ }
29
+ },
30
+ computed: {
31
+ componentType() {
32
+ return resolveWidgetSync(this.node.type) || EmptyFallback
33
+ },
34
+ boundProps() {
35
+ if (resolveWidgetSync(this.node.type)) {
36
+ return this.node.props
37
+ }
38
+ return { type: this.node.type }
39
+ }
40
+ }
41
+ }
42
+ </script>
@@ -0,0 +1,5 @@
1
+ /** provide / inject:编辑态 vs 预览态(渲染引擎侧) */
2
+ export const EDITOR_MODE = {
3
+ EDIT: 'edit',
4
+ PREVIEW: 'preview'
5
+ }
@@ -0,0 +1,58 @@
1
+ /** 按钮样式变体校验 */
2
+ export function isButtonVariant(value) {
3
+ return value === 'primary' || value === 'default'
4
+ }
5
+
6
+ function isApiParam(item) {
7
+ if (!item || typeof item !== 'object') return false
8
+ if (typeof item.name !== 'string') return false
9
+ if (item.source !== 'field' && item.source !== 'static') return false
10
+ if (item.source === 'field' && typeof item.fieldKey !== 'string') return false
11
+ if (item.source === 'static' && typeof item.value !== 'string') return false
12
+ return true
13
+ }
14
+
15
+ /** 接口绑定对象校验 */
16
+ export function isApiBinding(value) {
17
+ if (!value || typeof value !== 'object') return false
18
+ const method = value.method || 'GET'
19
+ if (typeof value.enabled !== 'boolean') return false
20
+ if (typeof value.url !== 'string') return false
21
+ if (method !== 'GET' && method !== 'POST') return false
22
+ if (value.params != null) {
23
+ if (!Array.isArray(value.params)) return false
24
+ if (!value.params.every(isApiParam)) return false
25
+ }
26
+ return true
27
+ }
28
+
29
+ export function createDefaultApiParam() {
30
+ return { name: '', source: 'field', fieldKey: '', value: '' }
31
+ }
32
+
33
+ export function createDefaultApiBinding() {
34
+ return {
35
+ enabled: false,
36
+ url: '',
37
+ method: 'POST',
38
+ params: []
39
+ }
40
+ }
41
+
42
+ function isSelectOption(item) {
43
+ if (!item || typeof item !== 'object') return false
44
+ return typeof item.label === 'string' && typeof item.value === 'string'
45
+ }
46
+
47
+ /** 下拉 options 校验 */
48
+ export function isSelectOptions(value) {
49
+ if (!Array.isArray(value)) return false
50
+ return value.every(isSelectOption)
51
+ }
52
+
53
+ export function createDefaultSelectOptions() {
54
+ return [
55
+ { label: '选项 A', value: 'a' },
56
+ { label: '选项 B', value: 'b' }
57
+ ]
58
+ }
@@ -0,0 +1,71 @@
1
+ import LcButton from '../components/base/LcButton.vue'
2
+ import LcInput from '../components/base/LcInput.vue'
3
+ import LcContainer from '../components/base/LcContainer.vue'
4
+ import LcSelect from '../components/base/LcSelect.vue'
5
+ import { createDefaultApiBinding, createDefaultSelectOptions } from '../utils/props'
6
+
7
+ export const widgetRegistry = {
8
+ button: {
9
+ component: LcButton,
10
+ meta: {
11
+ type: 'button',
12
+ title: '按钮',
13
+ defaultProps: {
14
+ label: '按钮',
15
+ type: 'primary',
16
+ disabled: false,
17
+ apiBinding: createDefaultApiBinding()
18
+ }
19
+ }
20
+ },
21
+ input: {
22
+ component: LcInput,
23
+ meta: {
24
+ type: 'input',
25
+ title: '输入框',
26
+ defaultProps: {
27
+ label: '用户名',
28
+ fieldKey: 'username',
29
+ value: '',
30
+ placeholder: '请输入',
31
+ disabled: false
32
+ }
33
+ }
34
+ },
35
+ container: {
36
+ component: LcContainer,
37
+ meta: {
38
+ type: 'container',
39
+ title: '栅格容器',
40
+ defaultProps: { columns: 2, gap: 12, padding: 12 }
41
+ }
42
+ },
43
+ select: {
44
+ component: LcSelect,
45
+ meta: {
46
+ type: 'select',
47
+ title: '下拉菜单',
48
+ defaultProps: {
49
+ label: '下拉菜单',
50
+ fieldKey: 'selectField',
51
+ value: '',
52
+ placeholder: '请选择',
53
+ disabled: false,
54
+ options: createDefaultSelectOptions()
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ export function resolveWidgetSync(type) {
61
+ return widgetRegistry[type]?.component || null
62
+ }
63
+
64
+ export function getWidgetMeta(type) {
65
+ return widgetRegistry[type]?.meta || null
66
+ }
67
+
68
+ export function buildNodeProps(type, overrides = {}) {
69
+ const meta = getWidgetMeta(type)
70
+ return { ...(meta?.defaultProps || {}), ...overrides }
71
+ }