Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
25 changes: 24 additions & 1 deletion packages/canvas/DesignCanvas/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@ import type { Node, RootNode } from '../../../types'

export type PageSchema = RootNode

export type AIHelperState = 'hidden' | 'chat' | 'loading' | 'confirm' | 'completed'
Comment thread
lichunn marked this conversation as resolved.

export interface NodeAIStatus {
state: AIHelperState
collapsed?: boolean // 面板是否收起(收起时保留原状态,重新打开可恢复)
aiContext?: any
lastAIAction?: string
aiHistory?: Array<{
timestamp: number
action: string
content: any
}>
chatContent?: string // 聊天内容
// AI采纳状态相关字段
originalNodeData?: any // AI修改前的节点数据备份
aiModifiedNodeData?: any // AI修改后的节点数据
}

export interface NodeStatus {
[key: string]: any
}

export interface PageState {
currentVm?: unknown
currentSchema?: { [x: string]: any; id: string }
Expand All @@ -17,7 +39,8 @@ export interface PageState {
isSaved: boolean
isLock: boolean
isBlock: boolean
nodesStatus: Record<string, any>
nodesStatus: Record<string, NodeStatus>
aiNodesStatus: Record<string, NodeAIStatus> // AI状态独立存储,避免与nodesStatus的可见性(false)冲突
loading: boolean
}

Expand Down
144 changes: 135 additions & 9 deletions packages/canvas/DesignCanvas/src/api/useCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
ChangePropsOperation,
DeleteOperation,
InsertOperation,
NodeAIStatus,
NodeOperation,
PageSchema,
PageState,
Expand All @@ -43,6 +44,7 @@ const defaultPageState: PageState = {
isLock: false,
isBlock: false,
nodesStatus: {},
aiNodesStatus: {},
loading: false
}

Expand Down Expand Up @@ -84,6 +86,39 @@ const rootSchema = ref([
}
])

// 初始化单个节点的AI状态(使用独立的aiNodesStatus,避免与nodesStatus可见性冲突)
const initializeNodeAIStatus = (node: Node, initialStatus: Partial<NodeAIStatus> = {}) => {
pageState.aiNodesStatus[node.id] = {
state: 'hidden',
originalNodeData: deepClone(node),
aiModifiedNodeData: undefined,
aiContext: null,
lastAIAction: '',
aiHistory: [],
...initialStatus
}
}

// 初始化所有现有节点的AI状态
const initializeAllNodesAIStatus = () => {
// 递归遍历 pageSchema 的 children 来初始化所有节点的AI状态
const traverseNodes = (nodes: any[]) => {
if (!nodes) return
nodes.forEach((node) => {
if (node.id && !pageState.aiNodesStatus[node.id]) {
initializeNodeAIStatus(node)
}
if (Array.isArray(node.children) && node.children.length) {
traverseNodes(node.children)
}
})
}

if (pageState.pageSchema?.children) {
traverseNodes(pageState.pageSchema.children)
}
Comment thread
lichunn marked this conversation as resolved.
}

const handleTinyGridColumnsSlots = (node: Node) => {
const columns = Array.isArray(node.props?.columns) ? node.props.columns : []
for (const columnItem of columns) {
Expand Down Expand Up @@ -176,13 +211,26 @@ const jsonDiffPatchInstance = jsonDiffPatch.create({
const { publish } = useMessage()

// 重置画布数据
const resetCanvasState = async (state: Partial<PageState> = {}) => {
// preserveAINodeStatus: 为true时保留aiNodesStatus并为新增节点补初始化(适用于AI/robot等schema热更新场景)
const resetCanvasState = async (state: Partial<PageState> = {}, options?: { preserveAINodeStatus?: boolean }) => {
const previousSchema = JSON.parse(JSON.stringify(pageState.pageSchema))
const preserveAINodeStatus = options?.preserveAINodeStatus ?? false

// 保留旧aiNodesStatus快照,用于后续diff补初始化
const oldAINodesStatus = preserveAINodeStatus ? { ...pageState.aiNodesStatus } : null

Object.assign(pageState, defaultPageState, state)

nodesMap.value.clear()

if (preserveAINodeStatus) {
// 保留aiNodesStatus,后续只为新增节点补初始化
pageState.aiNodesStatus = oldAINodesStatus
Comment thread
lichunn marked this conversation as resolved.
Outdated
} else {
// 切换页面时清空所有节点的AI状态,避免旧页面的AI状态残留
pageState.aiNodesStatus = {}
}

if (pageState.pageSchema) {
if (!pageState.pageSchema.children) {
pageState.pageSchema.children = []
Expand All @@ -200,6 +248,18 @@ const resetCanvasState = async (state: Partial<PageState> = {}) => {
nodesMap.value.set(0, { node: rootSchema.value, parent: pageState.pageSchema })

generateNodesMap(pageState.pageSchema.children, pageState.pageSchema)

if (preserveAINodeStatus) {
// 为新增的节点初始化AI状态(已存在的不覆盖)
nodesMap.value.forEach(({ node }) => {
if (node.id && !pageState.aiNodesStatus[node.id]) {
initializeNodeAIStatus(node)
}
})
} else {
// 初始化所有节点的AI状态
initializeAllNodesAIStatus()
}
}

const diffPatch = jsonDiffPatchInstance.diff(previousSchema, pageState.pageSchema)
Expand All @@ -208,6 +268,11 @@ const resetCanvasState = async (state: Partial<PageState> = {}) => {
publish({ topic: 'schemaImport', data: { current: pageState.pageSchema, previous: previousSchema, diffPatch } })
}

// 更新页面schema,保留AI状态(委托resetCanvasState + preserveAINodeStatus)
const updatePageSchema = (newPageSchema: any) => {
resetCanvasState({ ...pageState, pageSchema: newPageSchema }, { preserveAINodeStatus: true })
}
Comment thread
lichunn marked this conversation as resolved.

// 页面重置画布数据
const resetPageCanvasState = (state: Partial<PageState> = {}) => {
state.isBlock = false
Expand Down Expand Up @@ -383,10 +448,28 @@ const operationTypeMap = {

setNode(newNodeData, parentNode)

// 初始化新节点的AI状态
if (newNodeData.id) {
initializeNodeAIStatus(newNodeData)
}

// 6. 如果新节点有子节点,递归构建 nodeMap
if (Array.isArray(newNodeData?.children) && newNodeData.children.length > 0) {
const newNode = getNode(newNodeData.id)
generateNodesMap(newNodeData.children, newNode)

// 递归初始化所有子节点的AI状态
const initChildrenAIStatus = (children: Node[]) => {
children.forEach((child) => {
if (child.id) {
initializeNodeAIStatus(child)
}
if (Array.isArray(child?.children) && child.children.length > 0) {
initChildrenAIStatus(child.children)
}
})
}
initChildrenAIStatus(newNodeData.children)
Comment thread
lichunn marked this conversation as resolved.
Comment thread
lichunn marked this conversation as resolved.
}

// 7. 返回插入结果
Expand All @@ -410,16 +493,18 @@ const operationTypeMap = {
if (index > -1) {
parent.children.splice(index, 1)
nodesMap.value.delete(node.id)
delete pageState.aiNodesStatus[node.id]
}

let children = [...(node.children || [])]

// 递归清理 nodesMap
// 递归清理 nodesMap 和 aiNodesStatus
while (children?.length) {
const len = children.length
children.forEach((item) => {
const nodeItem = getNode(item.id)
nodesMap.value.delete(item.id)
delete pageState.aiNodesStatus[item.id]

if (Array.isArray(nodeItem?.children) && nodeItem?.children.length) {
children.push(...nodeItem.children)
Expand Down Expand Up @@ -597,7 +682,7 @@ const patchLatestSchema = (schema: unknown) => {
}
}

const importSchema = (data: any) => {
const importSchema = (data: any, options?: { preserveAINodeStatus?: boolean }) => {
let importData = data

if (typeof data === 'string') {
Expand All @@ -609,11 +694,7 @@ const importSchema = (data: any) => {
}
}

// JSON 格式校验
resetCanvasState({
...pageState,
pageSchema: importData
})
resetCanvasState({ ...pageState, pageSchema: importData }, options)
}

const exportSchema = () => {
Expand Down Expand Up @@ -650,6 +731,49 @@ const updateSchema = (data: Partial<PageSchema>) => {
publish({ topic: 'schemaChange', data: {} })
}

/**
* 恢复节点子树数据并重建nodesMap
* 用于AI回滚场景:恢复originalNodeData后需要同步清理/重建nodesMap
* @param nodeId 要恢复的节点ID
* @param restoredData 恢复后的节点数据(deepClone后的originalNodeData)
*/
const restoreNodeSubtree = (nodeId: string, restoredData: any) => {
// 1. 收集恢复前该节点子树中的所有ID(这些是需要从nodesMap中清理的)
const collectSubtreeIds = (node: any): string[] => {
const ids: string[] = []
if (node?.id) ids.push(node.id)
if (Array.isArray(node?.children)) {
node.children.forEach((child: any) => ids.push(...collectSubtreeIds(child)))
}
return ids
}

const currentNode = getNode(nodeId)
const oldIds = currentNode ? collectSubtreeIds(currentNode) : []
// 获取当前节点的parent信息(在清理前保存)
const parentEntry = nodesMap.value.get(nodeId)
const parentNode = parentEntry?.parent

// 2. 清理旧子树的nodesMap
oldIds.forEach((id) => nodesMap.value.delete(id))

// 3. 用恢复后的数据覆盖当前节点
if (currentNode) {
Object.keys(currentNode).forEach((key) => delete currentNode[key])
Object.assign(currentNode, restoredData)
}

// 4. 重建该节点自身的nodesMap条目
if (currentNode && parentNode) {
nodesMap.value.set(nodeId, { node: currentNode, parent: parentNode })
}

// 5. 重建子节点的nodesMap
if (Array.isArray(restoredData?.children) && restoredData.children.length && currentNode) {
generateNodesMap(restoredData.children, currentNode)
}
}

export default function () {
return {
pageState,
Expand Down Expand Up @@ -684,6 +808,8 @@ export default function () {
exportSchema,
getSchema,
getNodePath,
updateSchema
updateSchema,
updatePageSchema,
restoreNodeSubtree
}
}
Binary file added packages/canvas/container/assets/loading.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading