diff --git a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js
index 2f4c8e898e..2e147e3d33 100644
--- a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js
+++ b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js
@@ -70,7 +70,7 @@ export const checkHasSpecialType = (obj) => {
return false
}
-const handleJSExpressionBinding = (key, value, isJSX) => {
+const handleJSExpressionBinding = (key, value, isJSX, globalHooks) => {
const expressValue = value.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '')
if (isJSX) {
@@ -85,7 +85,19 @@ const handleJSExpressionBinding = (key, value, isJSX) => {
}
// expression 使用 v-bind 绑定
- return `:${key}="${expressValue}"`
+ if (expressValue.includes('"') && expressValue.includes("'")) {
+ let stateKey = `${key}_${randomString()}`
+ let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${expressValue}`)
+
+ while (!addSuccess) {
+ stateKey = `${key}_${randomString()}`
+ addSuccess = globalHooks.addState(stateKey, `${stateKey}:${expressValue}`)
+ }
+
+ return `:${key}="state.${stateKey}"`
+ } else {
+ return `:${key}="${expressValue.replaceAll(/"/g, "'")}"`
+ }
}
const handleBindI18n = (key, value, isJSX) => {
@@ -176,13 +188,25 @@ export const handleLoopAttrHook = (schemaData = {}, globalHooks, config) => {
if (loop?.value && loop?.type) {
source = loop.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '')
} else {
- source = JSON.stringify(loop).replaceAll("'", "\\'").replaceAll(/"/g, "'")
+ source = JSON.stringify(loop)
}
const iterVar = [...loopArgs]
if (!isJSX) {
- attributes.push(`v-for="(${iterVar.join(',')}) in ${source}"`)
+ if (source.includes('"') && source.includes("'")) {
+ let stateKey = `loop_${randomString()}`
+ let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${source}`)
+
+ while (!addSuccess) {
+ stateKey = `loop_${randomString()}`
+ addSuccess = globalHooks.addState(stateKey, `${stateKey}:${source}`)
+ }
+
+ attributes.push(`v-for="(${iterVar.join(',')}) in state.${stateKey}"`)
+ } else {
+ attributes.push(`v-for="(${iterVar.join(',')}) in ${source.replaceAll(/"/g, "'")}"`)
+ }
return
}
@@ -352,7 +376,7 @@ export const handleExpressionAttrHook = (schemaData, globalHooks, config) => {
Object.entries(props).forEach(([key, value]) => {
if (value?.type === JS_EXPRESSION && !isOn(key)) {
specialTypeHandler[JS_RESOURCE](value, globalHooks, config)
- attributes.push(handleJSExpressionBinding(key, value, isJSX))
+ attributes.push(handleJSExpressionBinding(key, value, isJSX, globalHooks))
delete props[key]
}
@@ -384,7 +408,7 @@ export const handleJSFunctionAttrHook = (schemaData, globalHooks, config) => {
functionName = value.value
}
- attributes.push(handleJSExpressionBinding(key, { value: functionName }, isJSX))
+ attributes.push(handleJSExpressionBinding(key, { value: functionName }, isJSX, globalHooks))
delete props[key]
}
@@ -451,11 +475,15 @@ const genStateAccessor = (value, globalHooks) => {
}
}
-const transformObjValue = (renderKey, value, globalHooks, config, transformObjType) => {
+const transformObjValue = (renderKey, value, globalHooks, config, transformObjType, shouldConvertQuote = false) => {
const result = { shouldBindToState: false, res: null }
if (typeof value === 'string') {
- result.res = `${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"`
+ if (shouldConvertQuote) {
+ result.res = `${renderKey}'${value.replaceAll(/"/g, "'").replaceAll(/'/g, "\\'")}'`
+ } else {
+ result.res = `${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"`
+ }
return result
}
@@ -468,7 +496,11 @@ const transformObjValue = (renderKey, value, globalHooks, config, transformObjTy
if (specialTypeHandler[value?.type]) {
const specialVal = specialTypeHandler[value.type](value, globalHooks, config)?.value || ''
- result.res = `${renderKey}${specialVal}`
+ if (shouldConvertQuote) {
+ result.res = `${renderKey}${specialVal.replaceAll(/"/g, "'")}`
+ } else {
+ result.res = `${renderKey}${specialVal}`
+ }
if (specialTypes.includes(value.type)) {
result.shouldBindToState = true
@@ -503,7 +535,7 @@ const transformObjValue = (renderKey, value, globalHooks, config, transformObjTy
}
const normalKeyRegexp = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
-export const transformObjType = (obj, globalHooks, config) => {
+export const transformObjType = (obj, globalHooks, config, shouldConvertQuote = false) => {
if (!obj || typeof obj !== 'object') {
return {
res: obj
@@ -527,7 +559,8 @@ export const transformObjType = (obj, globalHooks, config) => {
value,
globalHooks,
config,
- transformObjType
+ transformObjType,
+ shouldConvertQuote
)
if (tmpShouldBindToState) {
@@ -541,7 +574,7 @@ export const transformObjType = (obj, globalHooks, config) => {
// 复杂的 object 类型,需要递归处理
const { res: tempRes, shouldBindToState: tempShouldBindToState } =
- transformObjType(value, globalHooks, config) || {}
+ transformObjType(value, globalHooks, config, shouldConvertQuote) || {}
resStr.push(`${renderKey}${tempRes}`)
@@ -570,7 +603,7 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => {
return
}
- const { res, shouldBindToState } = transformObjType(value, globalHooks, config)
+ const { res, shouldBindToState } = transformObjType(value, globalHooks, config, true)
if (shouldBindToState && !isJSX) {
let stateKey = key
@@ -583,7 +616,7 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => {
attributes.push(`:${key}="state.${stateKey}"`)
} else {
- attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res.replaceAll(/"/g, "'")}"`)
+ attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res}"`)
}
delete props[key]
diff --git a/packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue b/packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue
index c515a9711d..b524540906 100644
--- a/packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue
+++ b/packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue
@@ -26,7 +26,7 @@ const state = vue.reactive({
nullValue: null,
numberValue: 0,
emptyStr: '',
- strVal: 'i am str.',
+ strVal: "i am 'str'.",
trueVal: true,
falseVal: false,
arrVal: [1, '2', { aaa: 'aaa' }, [3, 4], true, false],
diff --git a/packages/vue-generator/test/testcases/sfc/accessor/schema.json b/packages/vue-generator/test/testcases/sfc/accessor/schema.json
index 204121eec1..0935a984a8 100644
--- a/packages/vue-generator/test/testcases/sfc/accessor/schema.json
+++ b/packages/vue-generator/test/testcases/sfc/accessor/schema.json
@@ -44,7 +44,7 @@
}
},
"strVal": {
- "defaultValue": "i am str.",
+ "defaultValue": "i am 'str'.",
"accessor": {
"getter": {
"type": "JSFunction",
diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/components-map.json b/packages/vue-generator/test/testcases/sfc/templateQuote/components-map.json
new file mode 100644
index 0000000000..b84fe3b474
--- /dev/null
+++ b/packages/vue-generator/test/testcases/sfc/templateQuote/components-map.json
@@ -0,0 +1,9 @@
+[
+ {
+ "componentName": "TinyButton",
+ "exportName": "Button",
+ "package": "@opentiny/vue",
+ "version": "^3.10.0",
+ "destructuring": true
+ }
+]
diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue b/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue
new file mode 100644
index 0000000000..27197c4583
--- /dev/null
+++ b/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/page.schema.json b/packages/vue-generator/test/testcases/sfc/templateQuote/page.schema.json
new file mode 100644
index 0000000000..534af857a8
--- /dev/null
+++ b/packages/vue-generator/test/testcases/sfc/templateQuote/page.schema.json
@@ -0,0 +1,46 @@
+{
+ "state": {
+ "customAttrTest": {
+ "value": [
+ {
+ "defaultValue": "{\"class\": \"test-class\", \"id\": \"test-id\"}"
+ }
+ ]
+ }
+ },
+ "methods": {},
+ "componentName": "Page",
+ "css": "",
+ "props": {},
+ "lifeCycles": {},
+ "children": [
+ {
+ "componentName": "TinyButton",
+ "props": {
+ "type": "primary",
+ "text": "test",
+ "subStr": "pri\"ma\"ry'subStr'",
+ "customAttrTest": {
+ "value": [
+ {
+ "defaultValue": "{\"class\": \"test-class\", \"id\": \"test-id\", \"class2\": \"te'st'-class2\"}",
+ "subStr": "test-'cl'ass2"
+ }
+ ]
+ },
+ "customExpressionTest": {
+ "type": "JSExpression",
+ "value": "{\n \"value\": [\n {\n \"defaultValue\": \"{\\\"class\\\": \\\"test-class\\\", \\\"id\\\": \\\"test-id\\\"}\"\n }\n ]\n}"
+ }
+ },
+ "loopArgs": ["item", "index"],
+ "loop": {
+ "type": "JSExpression",
+ "value": "[\n {\n type: 'primary'\n, subStr: \"primary'subStr'\"\n },\n {\n type: \"\"\n },\n {\n type: \"info\"\n },\n {\n type: \"success\"\n },\n {\n type: \"warning\"\n },\n {\n type: \"danger\"\n }\n]"
+ },
+ "id": "63623253",
+ "children": []
+ }
+ ],
+ "fileName": "testTemplateQuote"
+}
diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/templateQuote.test.js b/packages/vue-generator/test/testcases/sfc/templateQuote/templateQuote.test.js
new file mode 100644
index 0000000000..4f6d0672e4
--- /dev/null
+++ b/packages/vue-generator/test/testcases/sfc/templateQuote/templateQuote.test.js
@@ -0,0 +1,33 @@
+import { expect, test, beforeEach, afterEach, vi } from 'vitest'
+import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc'
+import pageSchema from './page.schema.json'
+import { formatCode } from '@/utils/formatCode'
+
+let count = 0
+const mockValue = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
+
+beforeEach(() => {
+ // 伪随机数,保证每次快照都一致
+ vi.spyOn(global.Math, 'random').mockImplementation(() => {
+ const res = mockValue[count]
+
+ count++
+ if (count > 10) {
+ count = 0
+ }
+
+ return res
+ })
+})
+
+afterEach(() => {
+ vi.spyOn(global.Math, 'random').mockRestore()
+})
+
+test('should generate template quote correctly', async () => {
+ const res = genSFCWithDefaultPlugin(pageSchema, [])
+
+ const formattedCode = formatCode(res, 'vue')
+
+ await expect(formattedCode).toMatchFileSnapshot('./expected/templateQuote.vue')
+})