diff --git a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js
index 6914b149..075bb67a 100644
--- a/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js
+++ b/packages/vue-generator/src/generator/vue/sfc/genSetupSFC.js
@@ -21,7 +21,8 @@ import {
handleObjBindAttrHook,
handleEventAttrHook,
handleTinyIconPropsHook,
- handleJsxModelValueUpdate
+ handleJsxModelValueUpdate,
+ handleJSFunctionAttrHook
} from './generateAttribute'
import {
GEN_SCRIPT_HOOKS,
@@ -227,6 +228,7 @@ export const genSFCWithDefaultPlugin = (schema, componentsMap, config = {}, next
handleAttrKeyHook,
handlePrimitiveAttributeHook,
handleExpressionAttrHook,
+ handleJSFunctionAttrHook,
handleI18nAttrHook,
handleTinyIconPropsHook,
handleObjBindAttrHook,
diff --git a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js
index 6ec0eb14..2f4c8e89 100644
--- a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js
+++ b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js
@@ -1,3 +1,4 @@
+import { parse } from '@babel/parser'
import {
JS_EXPRESSION,
JS_FUNCTION,
@@ -358,6 +359,38 @@ export const handleExpressionAttrHook = (schemaData, globalHooks, config) => {
})
}
+export const handleJSFunctionAttrHook = (schemaData, globalHooks, config) => {
+ const { attributes, schema: { props = {} } = {} } = schemaData || {}
+ const isJSX = config.isJSX
+ Object.entries(props).forEach(([key, value]) => {
+ if (value?.type === JS_FUNCTION && !isOn(key)) {
+ let functionName = `${key}Fun`
+
+ try {
+ const ast = parse(value.value, { sourceType: 'module', plugins: ['typescript', 'jsx'] })
+ // 这里可能会存在命名冲突的场景,应该在设计态阻止命名冲突
+ functionName = ast.program.body[0].id.name
+
+ globalHooks.addStatement({
+ position: INSERT_POSITION.AFTER_METHODS,
+ value: `const ${functionName} = wrap(${value.value})`,
+ key: functionName
+ })
+ } catch (error) {
+ const logger = console
+ logger.warn(`解析失败属性 ${key} 失败: ${error.message || error}`)
+
+ // 解析失败,不处理,原样绑定到属性上
+ functionName = value.value
+ }
+
+ attributes.push(handleJSExpressionBinding(key, { value: functionName }, isJSX))
+
+ delete props[key]
+ }
+ })
+}
+
export const handleI18nAttrHook = (schemaData, globalHooks, config) => {
const { attributes, schema: { props = {} } = {} } = schemaData || {}
const isJSX = config.isJSX
diff --git a/packages/vue-generator/test/testcases/sfc/case01/expected/FormTable.vue b/packages/vue-generator/test/testcases/sfc/case01/expected/FormTable.vue
index 8328bbfe..dfdb6949 100644
--- a/packages/vue-generator/test/testcases/sfc/case01/expected/FormTable.vue
+++ b/packages/vue-generator/test/testcases/sfc/case01/expected/FormTable.vue
@@ -98,9 +98,9 @@ import { IconSearch, IconDel, iconHelpCircle, IconEdit } from '@opentiny/vue-ico
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'
-import CrmQuoteListGridStatus from '../components/CrmQuoteListGridStatus.vue'
+import CrmQuoteListGridStatus from '@/components/CrmQuoteListGridStatus.vue'
-import ImageTitle from '../components/ImageTitle.vue'
+import ImageTitle from '@/components/ImageTitle.vue'
const TinyIconSearch = IconSearch()
const TinyIconDel = IconDel()
diff --git a/packages/vue-generator/test/testcases/sfc/case02/expected/UsePropAccessor.vue b/packages/vue-generator/test/testcases/sfc/case02/expected/UsePropAccessor.vue
index 69bfc22b..95382f3a 100644
--- a/packages/vue-generator/test/testcases/sfc/case02/expected/UsePropAccessor.vue
+++ b/packages/vue-generator/test/testcases/sfc/case02/expected/UsePropAccessor.vue
@@ -11,7 +11,7 @@ import { Form as TinyForm, FormItem as TinyFormItem } from '@opentiny/vue'
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'
-import PropAccessor from '../components/PropAccessor.vue'
+import PropAccessor from '@/components/PropAccessor.vue'
const props = defineProps({})
diff --git a/packages/vue-generator/test/testcases/sfc/case03/expected/UseStateAccessor.vue b/packages/vue-generator/test/testcases/sfc/case03/expected/UseStateAccessor.vue
index 82302754..f4a3e4fa 100644
--- a/packages/vue-generator/test/testcases/sfc/case03/expected/UseStateAccessor.vue
+++ b/packages/vue-generator/test/testcases/sfc/case03/expected/UseStateAccessor.vue
@@ -12,7 +12,7 @@ import { Form as TinyForm, FormItem as TinyFormItem } from '@opentiny/vue'
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'
-import StateAccessor from '../components/StateAccessor.vue'
+import StateAccessor from '@/components/StateAccessor.vue'
const props = defineProps({})
diff --git a/packages/vue-generator/test/testcases/sfc/jsFunctionAttr/components-map.json b/packages/vue-generator/test/testcases/sfc/jsFunctionAttr/components-map.json
new file mode 100644
index 00000000..eda80675
--- /dev/null
+++ b/packages/vue-generator/test/testcases/sfc/jsFunctionAttr/components-map.json
@@ -0,0 +1,9 @@
+[
+ {
+ "componentName": "TinyTree",
+ "exportName": "Tree",
+ "package": "@opentiny/vue",
+ "version": "^3.10.0",
+ "destructuring": true
+ }
+]
diff --git a/packages/vue-generator/test/testcases/sfc/jsFunctionAttr/expected/jsFunctionAttrTest.vue b/packages/vue-generator/test/testcases/sfc/jsFunctionAttr/expected/jsFunctionAttrTest.vue
new file mode 100644
index 00000000..95b56185
--- /dev/null
+++ b/packages/vue-generator/test/testcases/sfc/jsFunctionAttr/expected/jsFunctionAttrTest.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
diff --git a/packages/vue-generator/test/testcases/sfc/jsFunctionAttr/jsFunctionAttr.test.js b/packages/vue-generator/test/testcases/sfc/jsFunctionAttr/jsFunctionAttr.test.js
new file mode 100644
index 00000000..bd7b6d5b
--- /dev/null
+++ b/packages/vue-generator/test/testcases/sfc/jsFunctionAttr/jsFunctionAttr.test.js
@@ -0,0 +1,12 @@
+import { expect, test } from 'vitest'
+import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc'
+import schema from './page.schema.json'
+import componentsMap from './components-map.json'
+import { formatCode } from '@/utils/formatCode'
+
+test('should generate extra jsFunction on setup', async () => {
+ const res = genSFCWithDefaultPlugin(schema, componentsMap)
+ const formattedCode = formatCode(res, 'vue')
+
+ await expect(formattedCode).toMatchFileSnapshot('./expected/jsFunctionAttrTest.vue')
+})
diff --git a/packages/vue-generator/test/testcases/sfc/jsFunctionAttr/page.schema.json b/packages/vue-generator/test/testcases/sfc/jsFunctionAttr/page.schema.json
new file mode 100644
index 00000000..64e00d32
--- /dev/null
+++ b/packages/vue-generator/test/testcases/sfc/jsFunctionAttr/page.schema.json
@@ -0,0 +1,73 @@
+{
+ "state": {},
+ "methods": {},
+ "componentName": "Page",
+ "css": "",
+ "props": {},
+ "lifeCycles": {},
+ "children": [
+ {
+ "componentName": "div",
+ "props": {},
+ "id": "85375559",
+ "children": [
+ {
+ "componentName": "TinyTree",
+ "props": {
+ "data": [
+ {
+ "label": "一级 1",
+ "children": [
+ {
+ "label": "二级 1-1",
+ "children": [
+ {
+ "label": "三级 1-1-1"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "label": "一级 2",
+ "children": [
+ {
+ "label": "二级 2-1",
+ "children": [
+ {
+ "label": "三级 2-1-1"
+ }
+ ]
+ },
+ {
+ "label": "二级 2-2",
+ "children": [
+ {
+ "label": "三级 2-2-1"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "className": "component-base-style",
+ "render-content": {
+ "type": "JSFunction",
+ "value": "function render(h, { node, data }) {\n return {node.label}\n}\n"
+ }
+ },
+ "children": [],
+ "id": "43235432"
+ }
+ ]
+ }
+ ],
+ "dataSource": {
+ "list": []
+ },
+ "utils": [],
+ "bridge": [],
+ "inputs": [],
+ "outputs": [],
+ "fileName": "jsFunctionAttrTest"
+}
diff --git a/packages/vue-generator/test/testcases/sfc/slotParams/expected/slotParamsPage.vue b/packages/vue-generator/test/testcases/sfc/slotParams/expected/slotParamsPage.vue
index bfa219de..34834d65 100644
--- a/packages/vue-generator/test/testcases/sfc/slotParams/expected/slotParamsPage.vue
+++ b/packages/vue-generator/test/testcases/sfc/slotParams/expected/slotParamsPage.vue
@@ -12,7 +12,7 @@
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'
-import BlockSlotParams from '../components/BlockSlotParams.vue'
+import BlockSlotParams from '@/components/BlockSlotParams.vue'
const props = defineProps({})