diff --git a/packages/common/component/MetaBindVariable.vue b/packages/common/component/MetaBindVariable.vue index 14e93bba4..27e92bd46 100644 --- a/packages/common/component/MetaBindVariable.vue +++ b/packages/common/component/MetaBindVariable.vue @@ -123,15 +123,14 @@ import { reactive, ref, computed, nextTick, watch } from 'vue' import { camelize, capitalize } from '@vue/shared' import { Button, DialogBox, Search, Switch, Input, Tooltip, Alert } from '@opentiny/vue' -import { useHttp } from '@opentiny/tiny-engine-http' -import { useCanvas, useResource, useLayout, useApp, useProperties, useData } from '@opentiny/tiny-engine-controller' +import { useCanvas, useProperties } from '@opentiny/tiny-engine-controller' import { theme } from '@opentiny/tiny-engine-controller/adapter' import { constants } from '@opentiny/tiny-engine-utils' import SvgButton from './SvgButton.vue' -import { parse, traverse, generate } from '@opentiny/tiny-engine-controller/js/ast' import { DEFAULT_LOOP_NAME } from '@opentiny/tiny-engine-controller/js/constants' import MonacoEditor from './VueMonaco.vue' import { formatString } from '@opentiny/tiny-engine-controller/js/ast' +import { register, getRegistrationArray } from '@opentiny/tiny-engine-entry' const { EXPRESSION_TYPE } = constants @@ -165,6 +164,43 @@ const getJsSlotParams = () => { return isJsSlot ? jsSlot?.params || [] : [] } +register( + 'VARIABLE_CONFIGURATOR_LIST', + { + loop: { + content: '循环变量', + condition: () => useProperties().getSchema()?.loop, + getVariables: () => { + const [loopItem = DEFAULT_LOOP_NAME.ITEM, loopIndex = DEFAULT_LOOP_NAME.INDEX] = + useProperties().getSchema()?.loopArgs || [] + + return { + bindPrefix: '', + variables: [loopItem, loopIndex].reduce((variables, param) => ({ ...variables, [param]: param }), {}) + } + }, + _order: 800 + }, + slotScope: { + id: 'slotScope', + content: '暴露给插槽使用的变量', + condition: () => { + const [isInJsSlot] = getJsSlot() + return isInJsSlot + }, + getVariables: () => { + const params = getJsSlotParams() + return { + bindPrefix: '', + variables: params.reduce((variables, param) => ({ ...variables, [param]: param }), {}) + } + }, + _order: 900 + } + }, + { mergeObject: true } +) + export default { name: 'MetaBindVariable', components: { @@ -195,20 +231,12 @@ export default { }, setup(props, { emit }) { const editor = ref(null) - const http = useHttp() let oldValue = '' + let postConfirm = null - const list = [ - { id: 'state', content: 'State 属性' }, - { id: 'store', content: '应用状态' }, - { id: 'function', content: '自定义处理函数' }, - { id: 'utils', content: '工具类' }, - { id: 'bridge', content: '桥接源' }, - { id: 'datasource', content: '数据源' } - ] + const list = getRegistrationArray('VARIABLE_CONFIGURATOR_LIST') const state = reactive({ - isBlock: computed(() => useCanvas().isBlock()), variables: {}, // 控制变量列表显示/隐藏 isVisible: false, @@ -216,24 +244,7 @@ export default { value: '', active: 'state', // 某一类型下的变量列表 - variableList: computed(() => { - const extendedVars = [] - const [isInJsSlot] = getJsSlot() - - if (state.isBlock) { - extendedVars.push({ id: 'props', content: 'props' }) - } - - if (state.loopData) { - extendedVars.push({ id: 'loop', content: '循环变量' }) - } - - if (isInJsSlot) { - extendedVars.push({ id: 'slotScope', content: '暴露给插槽使用的变量' }) - } - - return [...list, ...extendedVars] - }), + variableList: [], // 绑定的变量名/变量表达式 variable: '', // 绑定的变量指向的值内容 @@ -246,7 +257,6 @@ export default { // 静态值 mock: props.modelValue?.value || props.modelValue, bindPrefix: '', - loopData: null, loopArgs: '', isPoll: false, pollInterval: 5000 @@ -260,7 +270,9 @@ export default { (value) => { if (value) { oldValue = state.variable - state.loopData = useProperties().getSchema()?.loop + state.variableList = list.filter((item) => + typeof item.condition === 'function' ? item.condition(state) : true + ) } } ) @@ -297,79 +309,6 @@ export default { }) } - const removeInterval = (start, end, intervalId, pageSchema) => { - const unmountedFn = pageSchema.lifeCycles?.onUnmounted?.value - const fetchBody = ` - /** ${start} */ - clearInterval(state.${intervalId}); - /** ${end} */` - - if (!unmountedFn) { - pageSchema.lifeCycles = pageSchema.lifeCycles || {} - pageSchema.lifeCycles.onUnmounted = { - type: 'JSFunction', - value: `function onUnmounted() {${fetchBody}}` - } - } else { - if (!unmountedFn.includes(`${intervalId}`)) { - pageSchema.lifeCycles.onUnmounted.value = unmountedFn.trim().replace(/\}$/, fetchBody + '}') - } - } - } - - const genRemoteMethodToLifeSetup = (variableName, sourceRef, pageSchema) => { - if (sourceRef?.data?.data) { - const setupFn = pageSchema.lifeCycles?.setup?.value - const { getCommentByKey } = useData() - const { start, end } = getCommentByKey(variableName) - const intervalId = `${CONSTANTS.INTERVALID}${capitalize(camelize(sourceRef.name))}` - const isPoll = state.isPoll && state.pollInterval !== undefined - - let fetchBodyFn = `${CONSTANTS.DATASOURCEMAP}${sourceRef.name}.load().then(res => { - state.${variableName} = res?.data?.items || res?.data || res - })` - - if (isPoll) { - fetchBodyFn = `state.${intervalId} = setInterval(() => {${CONSTANTS.DATASOURCEMAP}${sourceRef.name}.load().then(res => { - state.${variableName} = res?.data?.items || res?.data || res - })}, ${state.pollInterval})` - } - - const fetchBody = ` - /** ${start} */ - ${fetchBodyFn}; - /** ${end} */` - - if (!setupFn) { - pageSchema.lifeCycles = pageSchema.lifeCycles || {} - pageSchema.lifeCycles.setup = { - type: 'JSFunction', - value: `function setup({ props, state, watch, onMounted }) {${fetchBody}}` - } - } else { - if (!setupFn.includes(`${CONSTANTS.DATASOURCEMAP}${sourceRef.name}`)) { - pageSchema.lifeCycles.setup.value = setupFn.trim().replace(/\}$/, fetchBody + '}') - } else { - const ast = parse(setupFn) - traverse(ast, { - ExpressionStatement(path) { - if (path.toString().includes(sourceRef.name)) { - path.replaceWithSourceString(fetchBodyFn) - path.stop() - } - } - }) - - pageSchema.lifeCycles.setup.value = generate(ast).code - } - } - - if (isPoll) { - removeInterval(start, end, intervalId, pageSchema) - } - } - } - const variableClick = (key, item) => { if (state.bindPrefix === CONSTANTS.DATASOUCEPREFIX) { // 当选中数据源时,直接生成对应state变量并绑定数据源的静态数据 @@ -403,7 +342,7 @@ export default { const confirm = () => { let variableContent = state.isEditorEditMode ? editor.value?.getEditor().getValue() : state.variable - const { setSaved, canvasApi } = useCanvas() + const { setSaved } = useCanvas() // 如果新旧值不一样就显示未保存状态 if (oldValue !== variableContent) { setSaved(false) @@ -414,18 +353,8 @@ export default { const needFetchDataFormat = props.name === 'fetchData' && !pattern.test(variableContent) if (variableContent) { - if (state.bindPrefix === CONSTANTS.DATASOUCEPREFIX) { - const pageSchema = canvasApi.value.getSchema() - const stateName = state.variable.replace(`${CONSTANTS.STATE}`, '') - const staticData = state.variableContent.map(({ _id, ...other }) => other) - - pageSchema.state[stateName] = staticData - - // 设置画布上下文环境,让画布触发更新渲染 - canvasApi.value.setState({ [stateName]: staticData }) - - // 这里在setup生命周期函数内部处理用户真实环境中的数据源请求 - genRemoteMethodToLifeSetup(stateName, state.dataSouce, pageSchema) + if (typeof postConfirm === 'function') { + postConfirm(state) } emit('update:modelValue', { @@ -465,71 +394,20 @@ export default { const selectItem = (item) => { state.active = item.id - const { canvasApi } = useCanvas() - - if (item.id === 'function') { - state.bindPrefix = CONSTANTS.THIS - const { PLUGIN_NAME, getPluginApi } = useLayout() - const { getMethods } = getPluginApi(PLUGIN_NAME.PageController) - state.variables = { ...getMethods?.() } - } else if (item.id === 'bridge' || item.id === 'utils') { - state.bindPrefix = `${CONSTANTS.THIS}${item.id}.` - const bridge = {} - useResource().resState[item.id]?.forEach((res) => { - bridge[res.name] = `${item.id}.${res.content.exportName}` - }) - - state.variables = bridge - } else if (item.id === 'props') { - state.bindPrefix = CONSTANTS.PROPS - const properties = canvasApi.value.getSchema()?.schema?.properties - const bindProperties = {} - properties?.forEach(({ content }) => { - content.forEach(({ property }) => { - bindProperties[property] = property - }) - }) - state.variables = bindProperties - } else if (item.id === 'datasource') { - state.bindPrefix = CONSTANTS.DATASOUCEPREFIX - const { appInfoState } = useApp() - const url = new URLSearchParams(location.search) - const selectedId = appInfoState.selectedId || url.get('id') - - // 实时请求数据源列表数据,保证数据源获取最新的数据源数据 - http.get(`/app-center/api/sources/list/${selectedId}`).then((data) => { - const sourceData = {} - data.forEach((res) => { - sourceData[res.name] = res - }) - state.variables = sourceData - }) - } else if (item.id === 'store') { - state.bindPrefix = CONSTANTS.STORE - state.variables = {} - - const stores = canvasApi.value.getGlobalState() - stores.forEach(({ id, state: storeState = {}, getters = {} }) => { - const loadProp = (prop) => { - const propBinding = `${id}.${prop}` - state.variables[propBinding] = propBinding - } - - Object.keys(storeState).forEach(loadProp) - Object.keys(getters).forEach(loadProp) + postConfirm = item.postConfirm + + if (typeof item.getVariables === 'function') { + const { bindPrefix, variables } = item.getVariables() + state.bindPrefix = bindPrefix + state.variables = variables + } else if (typeof item.getVariablesAsync === 'function') { + item.getVariablesAsync().then(({ bindPrefix, variables }) => { + state.bindPrefix = bindPrefix + state.variables = variables }) - } else if (item.id === 'loop') { - state.bindPrefix = '' - const [loopItem = DEFAULT_LOOP_NAME.ITEM, loopIndex = DEFAULT_LOOP_NAME.INDEX] = - useProperties().getSchema()?.loopArgs || [] - state.variables = [loopItem, loopIndex].reduce((variables, param) => ({ ...variables, [param]: param }), {}) - } else if (item.id === 'slotScope') { - state.bindPrefix = '' - const params = getJsSlotParams() - state.variables = params.reduce((variables, param) => ({ ...variables, [param]: param }), {}) } else { - state.bindPrefix = CONSTANTS.STATE - state.variables = canvasApi.value.getSchema()?.[item.id] + state.bindPrefix = '' + state.variables = {} } } diff --git a/packages/entry/src/common.js b/packages/entry/src/common.js index 535d7eed9..c2495084f 100644 --- a/packages/entry/src/common.js +++ b/packages/entry/src/common.js @@ -157,3 +157,47 @@ export const generateRegistry = (registry) => { export const getMergeMeta = (id) => { return metasHashMap[id] } + +// 全局注册数据 +const registration = {} + +/** + * 获取token对应的注册的数据 + * @param {string} token + * @returns + */ +export const getRegistration = (token) => registration[token] + +/** + * 获取token对应的注册的数组数据。数组实际上是以对象保存的,根据_order字段排序 + * @param {string} token + * @returns + */ +export const getRegistrationArray = (token) => { + const obj = getRegistration(token) + + if (typeof obj !== 'object' || obj === null) { + return [] + } + + const result = Object.entries(obj).map(([key, value]) => ({ ...value, id: key })) + + return result + .map(({ _order, ...rest }) => ({ ...rest, _order: _order ?? Number.MAX_SAFE_INTEGER })) + .sort((a, b) => a._order - b._order) +} + +/** + * 注册全局数据 + * @param {string} token + * @param {any} value + * @param {{ mergeObject: boolean }} + */ +export const register = (token, value, { mergeObject } = {}) => { + if (mergeObject) { + const registered = getRegistration(token) || {} + registration[token] = { ...registered, ...value } + } else { + registration[token] = value + } +} diff --git a/packages/entry/src/index.js b/packages/entry/src/index.js index e1ecd6165..5a9814edc 100644 --- a/packages/entry/src/index.js +++ b/packages/entry/src/index.js @@ -10,7 +10,7 @@ * */ -export { getMergeMeta, getPluginApi, getOptions } from './common' +export { getMergeMeta, getPluginApi, getOptions, register, getRegistration, getRegistrationArray } from './common' export { useCompile } from './templateHash' export { defineEntry, callEntry, beforeCallEntry, afterCallEntry, mergeRegistry, getMergeRegistry } from './entryHash' export { getLayoutComponent } from './layoutHash' diff --git a/packages/plugins/block/src/Main.vue b/packages/plugins/block/src/Main.vue index 337f756f1..acfaf8de6 100644 --- a/packages/plugins/block/src/Main.vue +++ b/packages/plugins/block/src/Main.vue @@ -134,6 +134,9 @@ import { publishBlock } from './js/blockSetting' import { fetchBlockList, requestBlocks, requestInitBlocks, fetchBlockContent } from './js/http' +import { registerVariableConfiguratorList } from './register' + +registerVariableConfiguratorList() const docsUrl = useHelp().getDocsUrl('block') const { SORT_TYPE } = constants diff --git a/packages/plugins/block/src/register.js b/packages/plugins/block/src/register.js new file mode 100644 index 000000000..ac1081552 --- /dev/null +++ b/packages/plugins/block/src/register.js @@ -0,0 +1,30 @@ +import { useCanvas } from '@opentiny/tiny-engine-controller' +import { register } from '@opentiny/tiny-engine-entry' + +export const registerVariableConfiguratorList = () => { + register( + 'VARIABLE_CONFIGURATOR_LIST', + { + props: { + content: 'props', + condition: () => useCanvas().isBlock(), + getVariables: () => { + const properties = useCanvas().canvasApi.value.getSchema()?.schema?.properties + const bindProperties = {} + properties?.forEach(({ content }) => { + content.forEach(({ property }) => { + bindProperties[property] = property + }) + }) + + return { + bindPrefix: 'this.props.', + variables: bindProperties + } + }, + _order: 700 + } + }, + { mergeObject: true } + ) +} diff --git a/packages/plugins/bridge/src/Main.vue b/packages/plugins/bridge/src/Main.vue index 2d808555d..fb4d585a7 100644 --- a/packages/plugins/bridge/src/Main.vue +++ b/packages/plugins/bridge/src/Main.vue @@ -28,6 +28,9 @@ import { RESOURCE_TYPE } from './js/resource' import BridgeManage from './BridgeManage.vue' import BridgeSetting, { openPanel, closePanel } from './BridgeSetting.vue' import { setType, RESOURCE_TIP } from './js/resource' +import { registerVariableConfiguratorList } from './register' + +registerVariableConfiguratorList() export default { components: { diff --git a/packages/plugins/bridge/src/register.js b/packages/plugins/bridge/src/register.js new file mode 100644 index 000000000..ee5196dd7 --- /dev/null +++ b/packages/plugins/bridge/src/register.js @@ -0,0 +1,33 @@ +import { useResource } from '@opentiny/tiny-engine-controller' +import { register } from '@opentiny/tiny-engine-entry' + +export const registerVariableConfiguratorList = () => { + register( + 'VARIABLE_CONFIGURATOR_LIST', + { + utils: { + content: '工具类', + getVariables: () => ({ + bindPrefix: 'this.utils.', + variables: (useResource().resState['utils'] || []).reduce((result, item) => { + result[item.name] = `utils.${item.content.exportName}` + return result + }, {}) + }), + _order: 400 + }, + bridge: { + content: '桥接源', + getVariables: () => ({ + bindPrefix: 'this.bridge.', + variables: (useResource().resState['bridge'] || []).reduce((result, item) => { + result[item.name] = `bridge.${item.content.exportName}` + return result + }, {}) + }), + _order: 500 + } + }, + { mergeObject: true } + ) +} diff --git a/packages/plugins/data/src/Main.vue b/packages/plugins/data/src/Main.vue index f13d926f2..40e9ea7e4 100644 --- a/packages/plugins/data/src/Main.vue +++ b/packages/plugins/data/src/Main.vue @@ -87,6 +87,9 @@ import CreateStore from './CreateStore.vue' import { updateGlobalState } from './js/http' import { STATE, OPTION_TYPE } from './js/constants' import { validateMonacoEditorData } from './js/common' +import { registerVariableConfiguratorList } from './register' + +registerVariableConfiguratorList() export default { components: { diff --git a/packages/plugins/data/src/register.js b/packages/plugins/data/src/register.js new file mode 100644 index 000000000..7cd58a2a3 --- /dev/null +++ b/packages/plugins/data/src/register.js @@ -0,0 +1,42 @@ +import { useCanvas } from '@opentiny/tiny-engine-controller' +import { register } from '@opentiny/tiny-engine-entry' + +export const registerVariableConfiguratorList = () => { + register( + 'VARIABLE_CONFIGURATOR_LIST', + { + state: { + content: 'State 属性', + getVariables: () => ({ + bindPrefix: 'this.state.', + variables: useCanvas().canvasApi.value.getSchema()?.state + }), + _order: 100 + }, + store: { + content: '应用状态', + getVariables: () => { + const variables = {} + + const stores = useCanvas().canvasApi.value.getGlobalState() + stores.forEach(({ id, state: storeState = {}, getters = {} }) => { + const loadProp = (prop) => { + const propBinding = `${id}.${prop}` + variables[propBinding] = propBinding + } + + Object.keys(storeState).forEach(loadProp) + Object.keys(getters).forEach(loadProp) + }) + + return { + bindPrefix: 'this.stores.', + variables + } + }, + _order: 200 + } + }, + { mergeObject: true } + ) +} diff --git a/packages/plugins/datasource/src/Main.vue b/packages/plugins/datasource/src/Main.vue index 1ec29d6c8..2a48d60a2 100644 --- a/packages/plugins/datasource/src/Main.vue +++ b/packages/plugins/datasource/src/Main.vue @@ -63,6 +63,9 @@ import DataSourceGlobalDataHandler, { open as openGlobalDataHander, close as closeGlobalDataHandler } from './DataSourceGlobalDataHandler.vue' +import { registerVariableConfiguratorList } from './register' + +registerVariableConfiguratorList() export default { components: { diff --git a/packages/plugins/datasource/src/register.js b/packages/plugins/datasource/src/register.js new file mode 100644 index 000000000..49b1156d4 --- /dev/null +++ b/packages/plugins/datasource/src/register.js @@ -0,0 +1,122 @@ +import { useApp, useCanvas, useData } from '@opentiny/tiny-engine-controller' +import { generate, parse, traverse } from '@opentiny/tiny-engine-controller/js/ast' +import { register } from '@opentiny/tiny-engine-entry' +import { camelize, capitalize } from '@vue/shared' +import { fetchDataSourceList } from './js/http' + +const removeInterval = (start, end, intervalId, pageSchema) => { + const unmountedFn = pageSchema.lifeCycles?.onUnmounted?.value + const fetchBody = ` + /** ${start} */ + clearInterval(state.${intervalId}); + /** ${end} */` + + if (!unmountedFn) { + pageSchema.lifeCycles = pageSchema.lifeCycles || {} + pageSchema.lifeCycles.onUnmounted = { + type: 'JSFunction', + value: `function onUnmounted() {${fetchBody}}` + } + } else { + if (!unmountedFn.includes(`${intervalId}`)) { + pageSchema.lifeCycles.onUnmounted.value = unmountedFn.trim().replace(/\}$/, fetchBody + '}') + } + } +} + +const genRemoteMethodToLifeSetup = (variableName, sourceRef, pageSchema, pollInterval) => { + if (!sourceRef?.data?.data) { + return + } + + const setupFn = pageSchema.lifeCycles?.setup?.value + const { getCommentByKey } = useData() + const { start, end } = getCommentByKey(variableName) + const intervalId = `intervalId${capitalize(camelize(sourceRef.name))}` + const isPoll = pollInterval > 0 + + let fetchBodyFn = `this.dataSourceMap.${sourceRef.name}.load().then(res => { + state.${variableName} = res?.data?.items || res?.data || res + })` + + if (isPoll) { + fetchBodyFn = `state.${intervalId} = setInterval(() => {this.dataSourceMap.${sourceRef.name}.load().then(res => { + state.${variableName} = res?.data?.items || res?.data || res + })}, ${pollInterval})` + } + + const fetchBody = ` + /** ${start} */ + ${fetchBodyFn}; + /** ${end} */` + + if (!setupFn) { + pageSchema.lifeCycles = pageSchema.lifeCycles || {} + pageSchema.lifeCycles.setup = { + type: 'JSFunction', + value: `function setup({ props, state, watch, onMounted }) {${fetchBody}}` + } + } else { + if (!setupFn.includes(`this.dataSourceMap.${sourceRef.name}`)) { + pageSchema.lifeCycles.setup.value = setupFn.trim().replace(/\}$/, fetchBody + '}') + } else { + const ast = parse(setupFn) + traverse(ast, { + ExpressionStatement(path) { + if (path.toString().includes(sourceRef.name)) { + path.replaceWithSourceString(fetchBodyFn) + path.stop() + } + } + }) + + pageSchema.lifeCycles.setup.value = generate(ast).code + } + } + + if (isPoll) { + removeInterval(start, end, intervalId, pageSchema) + } +} + +export const registerVariableConfiguratorList = () => { + register( + 'VARIABLE_CONFIGURATOR_LIST', + { + datasource: { + content: '数据源', + getVariablesAsync: () => { + const url = new URLSearchParams(location.search) + const selectedId = useApp().appInfoState.selectedId || url.get('id') + + return fetchDataSourceList(selectedId).then((data) => { + return { + bindPrefix: '数据源: ', + variables: data.reduce((result, item) => { + result[item.name] = item + return result + }, {}) + } + }) + }, + postConfirm: (state) => { + const { canvasApi } = useCanvas() + const pageSchema = canvasApi.value.getSchema() + const stateName = state.variable.replace('this.state.', '') + const staticData = state.variableContent.map(({ _id, ...other }) => other) + + pageSchema.state[stateName] = staticData + + // 设置画布上下文环境,让画布触发更新渲染 + canvasApi.value.setState({ [stateName]: staticData }) + + const pollInterval = state.isPoll ? state.pollInterval || -1 : -1 + // 这里在setup生命周期函数内部处理用户真实环境中的数据源请求 + genRemoteMethodToLifeSetup(stateName, state.dataSouce, pageSchema, pollInterval) + }, + _order: 600 + } + }, + { mergeObject: true } + ) +} diff --git a/packages/plugins/script/src/Main.vue b/packages/plugins/script/src/Main.vue index 9ef9e9ec3..a8c264468 100644 --- a/packages/plugins/script/src/Main.vue +++ b/packages/plugins/script/src/Main.vue @@ -34,6 +34,9 @@ import { initCompletion } from '@opentiny/tiny-engine-controller/js/completion' import { initLinter } from '@opentiny/tiny-engine-controller/js/linter' import { theme } from '@opentiny/tiny-engine-controller/adapter' import useMethod, { saveMethod, highlightMethod, getMethodNameList, getMethods } from './js/method' +import { registerVariableConfiguratorList } from './register' + +registerVariableConfiguratorList() export const api = { saveMethod, diff --git a/packages/plugins/script/src/register.js b/packages/plugins/script/src/register.js new file mode 100644 index 000000000..12e09463a --- /dev/null +++ b/packages/plugins/script/src/register.js @@ -0,0 +1,21 @@ +import { register } from '@opentiny/tiny-engine-entry' +import { getMethods } from './js/method' + +export const registerVariableConfiguratorList = () => { + register( + 'VARIABLE_CONFIGURATOR_LIST', + { + function: { + content: '自定义处理函数', + getVariables: () => { + return { + bindPrefix: 'this.', + variables: { ...getMethods() } + } + }, + _order: 300 + } + }, + { mergeObject: true } + ) +}