* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { ref, reactive, readonly, type DeepReadonly, toRaw } from 'vue'
import { hyphenate } from '@vue/shared'
import { extend, copyArray } from '@opentiny/vue-renderless/common/object'
import { format } from '@opentiny/vue-renderless/common/date'
import { remove } from '@opentiny/vue-renderless/common/array'
import { constants } from '@opentiny/tiny-engine-utils'
import { getCanvasStatus } from '@opentiny/tiny-engine-common/js/canvas'
import { ast2String, parseExpression } from '@opentiny/tiny-engine-common/js/ast'
import { getCssObjectFromStyleStr } from '@opentiny/tiny-engine-common/js/css'
import {
useCanvas,
useTranslate,
useBreadcrumb,
useLayout,
useMessage,
getMetaApi,
META_APP,
getMergeMeta,
getOptions,
META_SERVICE
} from '@opentiny/tiny-engine-meta-register'
import meta from '../../meta'
import type {
Block,
BlockContent,
BlockGroup,
BlockProperty,
CreateBlockOptions,
CreateEmptyBlockOptions,
ParseChildPropsOptions,
ParsePropToDataOptons,
Property,
SchemaData
} from './types'
const { SORT_TYPE, SCHEMA_DATA_TYPE, BLOCK_OPENNESS } = constants
const NODE_TYPE_PAGE = 'Page'
const nameCn = 'name_cn'
const DEFAULT_PROPERTIES = readonly<Property[]>([
{
label: {
zh_CN: '基础信息'
},
description: {
zh_CN: '基础信息'
},
collapse: {
number: 6,
text: {
zh_CN: '显示更多'
}
},
content: []
}
])
const DEFAULT_BLOCK = readonly<DeepReadonly<BlockContent>>({
componentName: 'Block',
fileName: '',
css: '',
props: {},
children: [],
schema: {
properties: DEFAULT_PROPERTIES,
events: {}
},
state: {},
methods: {},
dataSource: {}
})
const blockState = reactive<{ list: Block[]; current: Block | null }>({
list: [],
current: null
})
const groupState = reactive<{ list: BlockGroup[]; selected: BlockGroup | object }>({
list: [],
selected: {}
})
const categoryState = reactive<{ list: BlockGroup[] }>({
list: []
})
const getBlockList = () => blockState.list
const setBlockList = (list: Block[]) => {
blockState.list = list
}
const addBlock = (block: Block) => {
const blockList = getBlockList()
blockList.unshift(block)
}
const delBlock = (block: Block) => {
remove(getBlockList(), block)
}
const getCurrentBlock = () => blockState.current
const setCurrentBlock = (block: Block) => {
blockState.current = block
}
const getGroupList = () => groupState.list
const setGroupList = (list: BlockGroup[]) => {
groupState.list = list
}
const getCategoryList = () => categoryState.list
const setCategoryList = (list: BlockGroup[]) => {
categoryState.list = list
}
const getSelectedGroup = () => groupState.selected
const setSelectedGroup = (selected: BlockGroup) => {
groupState.selected = selected
}
const copyCss = (css: string, classNameList: string[]) => {
classNameList = Array.from(new Set(classNameList)).map((item) => '.' + item)
const cssObject = getCssObjectFromStyleStr(css)
let styleStr = ''
Object.entries(cssObject).forEach(([key, value]) => {
if (classNameList.some((classNameItem) => key.includes(classNameItem))) {
styleStr += `${key} {\n${value}\n}\n`
}
})
return styleStr
}
const copySchema = (schema: Partial<BlockContent['schema']>, contentList: string[], methods: Record<string, any>) => {
const content = schema?.properties?.[0]?.content || []
let emitList: string[] = []
const emitListCopies: Record<string, any> = {}
Object.keys(methods).forEach((key) => {
const item = JSON.stringify(methods[key].value).match(/emit..*?\)/g)
if (item?.length) {
emitList = [...emitList, ...item]
}
})
emitList.forEach((e) => {
const matches = e.match(/'.*?'/g)
if (!matches || !matches.length) {
return
}
let key = matches[0].replace(/'/g, '')
key = `on${key[0].toLocaleUpperCase() + key.slice(1, key.length)}`
if (schema?.events?.[key]) {
emitListCopies[key] = schema?.events[key]
}
})
const schemaCopies = {
properties: [
{
...extend(true, {}, DEFAULT_PROPERTIES[0]),
content: content.filter((item) => contentList.includes(item.property))
}
],
events: emitListCopies || {}
}
return schemaCopies
}
const copyMethods = (schema: Record<string, any>) => {
const methodsListCopies: Record<string, any> = {}
Object.entries(schema).forEach(([key, value]) => {
const ast: any = parseExpression(value.value)
if (ast.body?.body) {
ast.body.body = []
}
methodsListCopies[key] = {
type: 'JSFunction',
value: ast2String(ast)
}
})
return methodsListCopies
}
const copyState = (stateObj: Record<string, any> = {}, methodsObj: Record<string, any> = {}) => {
const stateCopies: Record<string, any> = {}
const stateKey = Object.keys(stateObj).map((e) => `state.${e} `)
stateKey.forEach((e) => {
Object.keys(methodsObj).forEach((key) => {
if (methodsObj[key].value.indexOf(e) !== -1) {
const key = e.replace('state.', '').replace(' ', '')
stateCopies[key] = stateObj[key]
}
})
})
return stateCopies
}
const parsePropToData = (data: SchemaData, { prop, langs, state, methods }: ParsePropToDataOptons) => {
if (prop.type === SCHEMA_DATA_TYPE.I18n) {
data.langs[prop.key] = langs[prop.key]
} else if (prop.type === SCHEMA_DATA_TYPE.JSExpression) {
if (/\.state\./.test(prop.value)) {
const key = prop.value.replace('this.state.', '')
data.state[key] = state[key]
} else if (/\.props\./.test(prop.value)) {
const key = prop.value.replace('this.props.', '')
data.contentList.push(key)
} else {
const key = prop.value.replace('this.', '').replace(/\(.*?\)/, '')
data.methods[key] = methods[key]
}
}
}
const filterDataFn =
(parseChildProps: (...args: any[]) => any) =>
({ children = [] as any[], langs = {}, methods = {}, state = {} }) => {
const data: SchemaData = {
langs: {},
methods: {},
state: {},
classNameList: [],
contentList: []
}
if (Array.isArray(children)) {
children.forEach((child) => {
parseChildProps(data, { child, langs, state, methods })
})
}
return data
}
const parseChildProps = (data: SchemaData, { child, langs, state, methods }: ParseChildPropsOptions) => {
if (child.props) {
Object.entries(child.props).forEach(([propKey, prop]) => {
if (typeof prop === 'object') {
parsePropToData(data, { prop, langs, state, methods })
} else {
if (propKey === 'className' && prop) {
data.classNameList.push(...prop.split(' ').filter((item: string) => item))
}
}
})
}
if (Array.isArray(child.children)) {
const filterData = filterDataFn(parseChildProps)
const childData = filterData({ children: child.children, langs, methods, state })
Object.assign(data.langs, childData.langs)
Object.assign(data.methods, childData.methods)
Object.assign(data.state, childData.state)
data.classNameList = [...data.classNameList, ...childData.classNameList]
data.contentList = [...data.contentList, ...childData.contentList]
}
}
const getBlockPageSchema = (block: Block) => {
const content = block?.content || {}
content.componentName = content.componentName || content.blockName || ''
return content
}
const initBlock = async (block: any = {}, _langs = {}, isEdit?: boolean) => {
const { resetBlockCanvasState, setSaved, getSchema } = useCanvas()
const { setBreadcrumbBlock } = useBreadcrumb()
await resetBlockCanvasState({ pageSchema: getBlockPageSchema(block) })
block.content = getSchema()
setCurrentBlock(block)
setBreadcrumbBlock([block[nameCn] || block.label])
if (!isEdit) {
block.occupier = getMetaApi(META_SERVICE.GlobalService).getState().userInfo
useLayout().layoutState.pageStatus = getCanvasStatus(block.occupier)
addBlock(block)
setSaved(false)
}
useMessage().publish({
topic: 'pageOrBlockInit',
data: block.content
})
}
const createBlock = ({ name_cn, label, path, categories }: CreateBlockOptions) => {
const { pageState } = useCanvas()
const rawSchema = toRaw(pageState.currentSchema)
let processedSchema = []
if (!rawSchema) {
processedSchema = []
} else if (Array.isArray(rawSchema)) {
processedSchema = rawSchema.map((schemaItem) => extend(true, {}, schemaItem))
} else {
processedSchema = extend(true, {}, rawSchema)
}
const isPageNode = processedSchema.componentName === NODE_TYPE_PAGE
const hasMultiSchema = Array.isArray(processedSchema) && processedSchema.length
const children = isPageNode ? processedSchema.children : hasMultiSchema ? processedSchema : [processedSchema]
const { getLangs } = useTranslate()
const filterData = filterDataFn(parseChildProps)
const { methods, state, classNameList, contentList } = extend(
true,
{},
filterData({
children,
langs: getLangs(),
methods: pageState.pageSchema?.methods,
state: pageState.pageSchema?.state
})
)
const css = copyCss(pageState.pageSchema?.css || '', classNameList)
const methodsCopies = copyMethods(methods)
Object.assign(methods, methodsCopies)
const schemaCopies = copySchema(pageState.pageSchema?.schema, contentList, methods)
const stateCopies = copyState(pageState.pageSchema?.state, methods)
Object.assign(state, stateCopies)
const block: Block = {
path,
[nameCn]: name_cn,
label,
histories: [],
categories,
public: BLOCK_OPENNESS.Open,
framework: getMergeMeta('engine.config')?.dslMode,
content: {
...extend(true, {}, DEFAULT_BLOCK),
fileName: label,
css,
methods,
state,
children,
schema: schemaCopies
}
}
const api = getMetaApi(META_APP.BlockManage)
return api.saveBlock?.(block)
}
const createEmptyBlock = ({ name_cn, label, path, categories }: CreateEmptyBlockOptions) => {
const block: Block = {
path,
[nameCn]: name_cn,
label,
categories,
public: BLOCK_OPENNESS.Open,
framework: getMergeMeta('engine.config')?.dslMode,
content: {
...extend(true, {}, DEFAULT_BLOCK),
fileName: label
}
}
const api = getMetaApi(META_APP.BlockManage)
return api.saveBlock?.(block)
}
const setComponentLinkedValue = ({ propertyName, value }: { propertyName: string; value: any }) => {
const { schema } = useCanvas().canvasApi.value?.getCurrent?.() || {}
if (!propertyName || !schema) {
return
}
schema.props = schema.props || {}
schema.props[propertyName] = value
}
const getBlockI18n = (block: Block) => block?.content?.i18n || {}
const getBlockProperties = (block: Block) => block?.content?.schema?.properties?.[0]?.content || []
const addBlockProperty = (property: BlockProperty, block: Block) => {
if (!block) {
return
}
if (!block.content) {
block.content = {} as BlockContent
}
if (!block.content.schema) {
block.content.schema = {} as BlockContent['schema']
}
if (!block.content.schema.properties) {
block.content.schema.properties = copyArray(DEFAULT_PROPERTIES)
}
block.content.schema.properties?.[0].content?.push(property)
if (property.linked) {
setComponentLinkedValue({
propertyName: property.linked.property,
value: {
type: SCHEMA_DATA_TYPE.JSExpression,
value: `this.props.${property.property}`
}
})
}
}
const editBlockProperty = (property: BlockProperty, data: any) => {
if (property.linked) {
const value = {
type: SCHEMA_DATA_TYPE.JSExpression,
value: `this.props.${property.property}`
}
setComponentLinkedValue({
propertyName: data?.property,
value
})
data.widget.props.modelValue = value
}
}
const removePropertyLink = ({ componentProperty }: { componentProperty: BlockProperty }) => {
const linked = componentProperty.linked
componentProperty.linked = null
const properties = getBlockProperties(getCurrentBlock()!)
properties.forEach((property) => {
if (property.linked && property.property === linked?.blockProperty) {
if (componentProperty.widget?.props?.modelValue) {
componentProperty.widget.props.modelValue = property.defaultValue
}
setComponentLinkedValue({
propertyName: property.linked.property,
value: property.defaultValue
})
property.linked = null
}
})
}
const getBlockEvents = (block = {} as Block) => block?.content?.schema?.events || {}
const addBlockEvent = ({ name, event }: { name: string; event: any }, block: Block) => {
if (!block) {
return
}
if (!block.content) {
block.content = {} as BlockContent
}
if (!block.content.schema) {
block.content.schema = {} as BlockContent['schema']
}
if (!block.content.schema.events) {
block.content.schema.events = {}
}
block.content.schema.events[name] = event
}
const removeEventLink = (linkedEventName: string) => {
const events = getBlockEvents(getCurrentBlock()!)
Object.entries(events).forEach(([name, event]) => {
if (linkedEventName === name) {
event.linked = null
}
})
}
const appendEventEmit = ({ eventName, functionName }: { eventName?: string; functionName?: string } = {}) => {
if (!eventName || !functionName) {
return
}
const getMethods = getMetaApi(META_APP.Page)?.getMethods
if (getMethods && typeof getMethods === 'function') {
const method = getMethods()?.[functionName]
if (method?.type === SCHEMA_DATA_TYPE.JSFunction) {
const ast: any = parseExpression(method.value)
const params = ast.params.map((param: { name: string }) => param.name)
const emitContent = `this.emit('${hyphenate(eventName.replace(/^on/i, ''))}', ${params.join(',')})`
if (!method?.value?.includes(emitContent)) {
ast.body.body.push(parseExpression(emitContent))
}
method.value = ast2String(ast)
}
}
}
const DEFAULT_GROUPS = [
{
groupId: 'all',
groupName: '所有分组'
},
{
groupId: 'default',
groupName: '设计器默认区块分组'
}
]
const DEFAULT_GROUP_ID = DEFAULT_GROUPS[1].groupId
const DEFAULT_GROUP_NAME = DEFAULT_GROUPS[1].groupName
const selectedGroup = ref({ ...DEFAULT_GROUPS[0] })
const selectedBlock = ref('')
const selectedBlockArray = ref<Block[]>([])
const isRefresh = ref(false)
const groupChange = (group?: BlockGroup) => {
if (!group) return
selectedGroup.value = {
groupId: group.groupId || group.id,
groupName: group.groupName || group.name
}
}
const addDefaultGroup = (groups: BlockGroup[]) => {
const result = DEFAULT_GROUPS.map((group) => ({
label: group.groupName,
value: group
}))
groups.forEach((item) => {
result.push({
label: item.name,
value: {
groupId: item.id,
groupName: item.name
}
})
})
setGroupList(groups)
return result
}
const isDefaultGroupId = (groupId: string) => groupId === DEFAULT_GROUP_ID
const isAllGroupId = (groupId: string) => groupId === DEFAULT_GROUPS[0].groupId
const getCurrentDate = () => new Date().setHours(0, 0, 0, 0)
interface DateInfo {
nowDayOfWeek: number
nowDay: number
nowMonth: number
nowYear: number
lastMonth: number
}
const getCurrentWeek = (date: DateInfo) => {
const { nowDayOfWeek, nowDay, nowMonth, nowYear } = date
const weekStartDate = new Date(nowYear, nowMonth, nowDay - nowDayOfWeek + 1)
return weekStartDate.setHours(0, 0, 0, 0)
}
const getCurrentMonth = (date: DateInfo) => {
const { nowMonth, nowYear } = date
const monthStartDate = new Date(nowYear, nowMonth, 1)
return monthStartDate.setHours(0, 0, 0, 0)
}
const getLastMonth = (date: DateInfo) => {
const { nowYear, lastMonth } = date
const lastMonthStartDate = new Date(nowYear, lastMonth, 1)
return lastMonthStartDate.setHours(0, 0, 0, 0)
}
const getDateFromNow = (timeStamp: number = 0) => {
const now = new Date()
const nowDay = now.getDate()
const nowMonth = now.getMonth()
const nowYear = now.getFullYear()
const nowDayOfWeek = now.getDay() || 7
const lastMonthDate = new Date()
lastMonthDate.setDate(1)
lastMonthDate.setMonth(lastMonthDate.getMonth() - 1)
const lastMonth = lastMonthDate.getMonth()
const date: DateInfo = { nowDayOfWeek, nowDay, nowMonth, nowYear, lastMonth }
const dateMap = new Map([
['今天', getCurrentDate],
['本周', () => getCurrentWeek(date)],
['本月', () => getCurrentMonth(date)],
['上月', () => getLastMonth(date)],
['更久以前', () => 0]
])
for (const [key, value] of dateMap) {
if (timeStamp >= value()) {
return key
}
}
return undefined
}
const splitBackupGroups = (data: { updated_at: string | number; message: string; id: string }[]) => {
const backupList: Record<string, any> = {}
if (!data || !data.length) return backupList
data.sort((backup1, backup2) => new Date(backup2.updated_at).getTime() - new Date(backup1.updated_at).getTime())
data.forEach((item) => {
const updateTime = item.updated_at ? new Date(item.updated_at) : null
const title = getDateFromNow(updateTime?.getTime()) || ''
backupList[title] = backupList[title] || []
backupList[title].push({
backupTitle: item.message,
backupTime: format(updateTime),
id: item.id
})
})
return backupList
}
const sortTypeHandlerMap = {
[SORT_TYPE.timeAsc]: (blockList: Block[]) => {
blockList.sort(
(block1, block2) => new Date(block1.updated_at || '').getTime() - new Date(block2.updated_at || '').getTime()
)
},
[SORT_TYPE.timeDesc]: (blockList: Block[]) => {
blockList.sort(
(block1, block2) => new Date(block2.updated_at || '').getTime() - new Date(block1.updated_at || '').getTime()
)
},
[SORT_TYPE.alphabetDesc]: (blockList: Block[]) => {
blockList.sort((block1, block2) => (block2.name_cn || block2.label).localeCompare(block1.name_cn || block1.label))
},
[SORT_TYPE.alphabetAsc]: (blockList: Block[]) => {
blockList.sort((block1, block2) => (block1.name_cn || block1.label).localeCompare(block2.name_cn || block2.label))
}
}
const sort = (blockList: Block[], type: string) => {
if (blockList.length === 0) return blockList
if (sortTypeHandlerMap[type]) {
sortTypeHandlerMap[type](blockList)
} else {
sortTypeHandlerMap[SORT_TYPE.timeDesc](blockList)
}
return blockList
}
const check = (block: Block) => {
if (selectedBlockArray.value.some((item) => item.id === block.id)) {
return
}
selectedBlockArray.value = selectedBlockArray.value.concat(block)
}
const cancelCheck = (block: Block) => {
selectedBlockArray.value = selectedBlockArray.value.filter((item) => item.id !== block.id)
}
const checkAll = (blockList: Block[]) => {
selectedBlockArray.value = blockList
}
const cancelCheckAll = () => {
selectedBlockArray.value = []
}
const shouldReplaceCategoryWithGroup = () => {
const { mergeCategoriesAndGroups } = getOptions(meta.id)
return mergeCategoriesAndGroups
}
export default function () {
return {
NODE_TYPE_PAGE,
DEFAULT_GROUP_ID,
DEFAULT_GROUP_NAME,
selectedGroup,
selectedBlock,
selectedBlockArray,
isRefresh,
addBlock,
delBlock,
createBlock,
createEmptyBlock,
groupChange,
addDefaultGroup,
isDefaultGroupId,
isAllGroupId,
splitBackupGroups,
sort,
check,
cancelCheck,
checkAll,
cancelCheckAll,
getBlockList,
setBlockList,
getBlockI18n,
getGroupList,
setGroupList,
getCategoryList,
setCategoryList,
addBlockEvent,
getBlockEvents,
appendEventEmit,
getCurrentBlock,
initBlock,
setCurrentBlock,
removeEventLink,
getSelectedGroup,
setSelectedGroup,
addBlockProperty,
editBlockProperty,
removePropertyLink,
getBlockProperties,
getBlockPageSchema,
getDateFromNow,
shouldReplaceCategoryWithGroup
}
}