Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 47 additions & 14 deletions packages/vue-generator/src/generator/vue/sfc/generateAttribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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]
}
Expand Down Expand Up @@ -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]
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -527,7 +559,8 @@ export const transformObjType = (obj, globalHooks, config) => {
value,
globalHooks,
config,
transformObjType
transformObjType,
shouldConvertQuote
)

if (tmpShouldBindToState) {
Expand All @@ -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}`)

Expand Down Expand Up @@ -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
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
}
},
"strVal": {
"defaultValue": "i am str.",
"defaultValue": "i am 'str'.",
"accessor": {
"getter": {
"type": "JSFunction",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"componentName": "TinyButton",
"exportName": "Button",
"package": "@opentiny/vue",
"version": "^3.10.0",
"destructuring": true
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<template>
<div>
<tiny-button
v-for="(item, index) in state.loop_6cio"
type="primary"
text="test"
subStr="pri'ma'ry'subStr'"
:customExpressionTest="{
value: [
{
defaultValue: '{\'class\': \'test-class\', \'id\': \'test-id\'}'
}
]
}"
:customAttrTest="{
value: [
{
defaultValue: '{\'class\': \'test-class\', \'id\': \'test-id\', \'class2\': \'te\'st\'-class2\'}',
subStr: 'test-\'cl\'ass2'
}
]
}"
></tiny-button>
</div>
</template>

<script setup>
import * as vue from 'vue'
import { defineProps, defineEmits } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'

const props = defineProps({})

const emit = defineEmits([])
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
const wrap = lowcodeWrap(props, { emit })
wrap({ stores })

const state = vue.reactive({
loop_6cio: [
{
type: 'primary',
subStr: "primary'subStr'"
},
{
type: ''
},
{
type: 'info'
},
{
type: 'success'
},
{
type: 'warning'
},
{
type: 'danger'
}
],
customAttrTest: { value: [{ defaultValue: "{'class': 'test-class', 'id': 'test-id'}" }] }
})
wrap({ state })
</script>
<style scoped></style>
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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')
})