refactor: optimize Vue canvas interaction logic#1326
Conversation
…tioning and interaction methods 重构: 优化 Vue 画布交互逻辑,提取元素定位和交互方法
WalkthroughThis update introduces a comprehensive refactor of the canvas container's selection and hover logic, transitioning from a custom multi-selection state management system to a modular, composable-driven approach using new Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CanvasContainer
participant useHoverNode
participant useSelectNode
participant CanvasState
participant CanvasHover/InsertLine
User->>CanvasContainer: Mouse event (mousedown/mouseover/contextmenu)
CanvasContainer->>useHoverNode: updateHoverNode(event)
useHoverNode->>CanvasContainer: Updates curHoverState
CanvasContainer->>useSelectNode: updateSelectedNode(event, type, isMultiple)
useSelectNode->>CanvasState: Update selection state
useSelectNode->>CanvasContainer: Updates selectState
CanvasContainer->>CanvasHover/InsertLine: Passes updated hover/selection state
CanvasHover/InsertLine->>User: Renders overlays and UI
Poem
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 7
🔭 Outside diff range comments (2)
packages/canvas/container/src/components/CanvasAction.vue (1)
329-338:⚠️ Potential issueVertical alignment uses
horizontalValueinstead ofverticalValue.Inside the
Align.align()method, the branch handling vertical positions (top/bottom) mistakenly assigns the offset tohorizontalValue.
Consequently, the computed style string never updatesverticalValue, so the option / label bars can appear mis-aligned when they have to be shifted vertically.- if (positions.isVertical(position)) { - this.alignTop = position === positions.TOP - this.horizontalValue = value - return this - } + if (positions.isVertical(position)) { + this.alignTop = position === positions.TOP + this.verticalValue = value + return this + }packages/canvas/container/src/CanvasContainer.vue (1)
121-141: 💡 Verification agent🧩 Analysis chain
Dragging logic ignores multi-selection & can mis-start drags
handleNodeInteractionsalways readsselectState.value[0]after an (async)updateSelectedNode.
If the user Ctrl/⌘-clicks several nodes, only the first item is ever considered when:
- deciding whether to start
dragStart- calculating the offset for that drag
This makes multi-select look supported but behave unpredictably, and the TODO a few lines below confirms it.
Consider short-circuiting the drag when
isMultipleSelectis true or extendingdragStartso it receives an array of selected nodes.
🏁 Script executed:
#!/bin/bash # Search for updateSelectedNode implementation to see multi-select handling echo "---- updateSelectedNode occurrences ----" rg "updateSelectedNode" -n . # Search for dragStart implementation to see parameter expectations echo "---- dragStart occurrences ----" rg "dragStart" -n . # Search for selectState declaration to confirm its shape and usage echo "---- selectState occurrences ----" rg "selectState" -n .Length of output: 11224
Update drag logic to properly handle multi-selection in CanvasContainer.vue
The current
handleNodeInteractionsalways usesselectState.value[0]afterupdateSelectedNode, so even whenisMultipleSelectis true you only ever drag the first node. You’ll need to either:
- Skip initiating a drag when multiple nodes are selected
e.g. at the top of the handler:if (isMultipleSelect) return- Or extend
dragStartto accept an array of selected nodes
and invoke it for each entry inselectState.value.Locations to address:
- packages/canvas/container/src/CanvasContainer.vue
Lines ~126–134 where you read fromselectState.value[0]fornode,element, andrect.- The call to
dragStart(node, element, { offsetX, offsetY })needs to be updated per the approach above.
♻️ Duplicate comments (1)
packages/canvas/container/src/interactions/common.ts (1)
115-125: Same issue as above forselectNodeByIdThe fake
MouseEventshould be replaced with a typed parameter or a real event as explained in the previous comment.
🧹 Nitpick comments (12)
packages/canvas/container/src/components/CanvasAction.vue (2)
382-457: Parameter naming makes intent unclear & risks leaks of extra props.
getStyleValuesreceives aselectStateobject but only de-structuresleft, top, width, height.
Call-sites pass a hand-crafted object ({ left, top, … }), not the whole reactiveprops.selectState.rect.
Consider renaming the first argument to{ left, top, width, height }(or destructure directly in the signature) to remove any ambiguity.
459-495: watchPostEffect may run more often than necessary.
watchPostEffectdepends onselectState.rectandshowQuickAction.value.
When the user drags-to-resize the selection rectangle, the expensiveawait nextTick()& DOM-measurement logic is executed on every mouse-move frame.If jank is observed, throttling the handler or switching to an explicit
watchwith a deep‐false shallow-reference torectchanges would help.packages/canvas/container/src/interactions/index.ts (1)
40-56: Minor: avoid wrapping plain objects in a Vuerefwhen no reactivity is required.
interactionsFnnever appears in a template or another reactive computation; a simple module-scoped variable would suffice and spare the tiny overhead of Vue’s proxy.-const interactionsFn = ref<IInteractionHooksMap[keyof IInteractionHooksMap] | null>(null) +let interactionsFn: IInteractionHooksMap[keyof IInteractionHooksMap] | null = nullpackages/canvas/container/src/interactions/common.ts (2)
71-75: Remove leftover “QUESTION” comment before mergingThe inline note
// QUESTION: 为什么要判断 node Type?looks like an internal discussion rather than documentation and will confuse future readers.
Please delete it or turn it into a proper JSDoc explanation.
107-113: Avoid forgingMouseEventobjects – pass theElementinstead
hoverNodeByIdfabricates a{ target: element } as unknown as MouseEvent.
The object misses many properties realMouseEvents provide and may break consumers
relying on them (e.g. modifiers,clientX, …).Two safer alternatives:
Change the callback signature to accept an
Element:hoverNodeById(id: string, cb: (el: Element) => void)Or, if you really need an event, create one properly:
const evt = new MouseEvent('mousemove', { bubbles: true }) Object.defineProperty(evt, 'target', { value: element }) cb(evt)Option 1 keeps the API simple and avoids unnecessary event creation.
packages/canvas/container/src/interactions/vue-interactions.ts (1)
219-238: PreferrequestAnimationFrameto throttledsetTimeoutfor rect refresh
updateSelectedRectusessetTimeout(…, 0)which:• Runs after all micro-tasks, not necessarily just after the next paint.
• Can queue multiple redundant executions during fast updates.Replacing it with
requestAnimationFrameties the recalculation to the browser’s
render cycle and avoids needless work:-const updateSelectedRect = (): void => { - setTimeout(() => { +const updateSelectedRect = (): void => { + cancelAnimationFrame((updateSelectedRect as any)._rafId) // clear previous + ;(updateSelectedRect as any)._rafId = requestAnimationFrame(() => { … - }, 0) + }) }This keeps the UI responsive, especially while dragging or resizing.
packages/canvas/container/src/CanvasContainer.vue (3)
251-258: Hot spot:mouseoverfires for every nested element – throttle or switch tomousemoveReplacing the old
mousemovelistener withmouseovermeans the callback now executes once per element the pointer traverses (often dozens of times per pixel).
Even with a lightweightupdateHoverNode, this will:
- allocate more JS objects
- trigger more DOM reads
- consume additional CPU on large documents
A quick fix is to debounce / throttle the handler with
requestAnimationFrame; longer-term you might revert topointermoveormousemovewhich are naturally rate-limited.
310-312: Dead-code TODO – remove or implement
selectSlotis still wired to<canvas-action @select-slot>but its body is commented out.
Callers silently no-op, making future debugging harder. Please either:
- implement the required behaviour, or
- remove the prop / event altogether.
343-346: Expose only what the template uses
selectStateandcurHoverStateare provided fromsetup()but never referenced in the<template>except as read-only props to children.
If you want to enforce encapsulation you can expose only the computed versions actually consumed (multiStateLength,computedSelectState, etc.) and keep the raw refs private.packages/canvas/container/src/container.ts (3)
321-332: Guard against undefined component names
getConfigure(undefined)is possible whenquerySelectByIdfails to locate a node.
controller.getMaterial(undefined)may throw or log noisy errors in some registries.-export const getConfigure = (targetName: string) => { - const material = getController().getMaterial(targetName) +export const getConfigure = (targetName?: string) => { + if (!targetName) { + return {} + } + const material = getController().getMaterial(targetName)
386-392: Scroll handler wipes hover but not insert-line overlay
updateRectclears hover state on scroll but leaveslineStateintact.
If the user scrolls while a drag is in progress, the insertion guideline can freeze onscreen.Either clear
lineStatehere or insideclearHover()to avoid stale UI.
551-556: Avoid querying onundefinedid
hoverNodeById(curHoverState.value?.node?.id)is invoked even when no node is hovered, resulting in a selector[data-uid="undefined"].-const { hoverNodeById, curHoverState } = useHoverNode() -hoverNodeById(curHoverState.value?.node?.id) +const { hoverNodeById, curHoverState } = useHoverNode() +const hoverId = curHoverState.value?.node?.id +if (hoverId) hoverNodeById(hoverId)Reduces needless DOM queries.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (23)
designer-demo/engine.config.js(1 hunks)designer-demo/public/mock/bundle.json(2 hunks)packages/canvas/container/index.ts(1 hunks)packages/canvas/container/src/CanvasContainer.vue(9 hunks)packages/canvas/container/src/components/CanvasAction.vue(6 hunks)packages/canvas/container/src/components/CanvasDivider.vue(2 hunks)packages/canvas/container/src/components/CanvasHover.vue(1 hunks)packages/canvas/container/src/components/CanvasInsertLine.vue(1 hunks)packages/canvas/container/src/components/CanvasResizeBorder.vue(2 hunks)packages/canvas/container/src/components/CanvasRouterJumper.vue(1 hunks)packages/canvas/container/src/components/CanvasViewerSwitcher.vue(1 hunks)packages/canvas/container/src/composables/useMultiSelect.ts(0 hunks)packages/canvas/container/src/container.ts(15 hunks)packages/canvas/container/src/interactions/common.ts(1 hunks)packages/canvas/container/src/interactions/html-interactions.ts(1 hunks)packages/canvas/container/src/interactions/index.ts(1 hunks)packages/canvas/container/src/interactions/vue-interactions.ts(1 hunks)packages/canvas/container/src/interactions/vue-rect.ts(1 hunks)packages/canvas/container/src/keyboard.ts(5 hunks)packages/canvas/render/src/render.ts(0 hunks)packages/canvas/render/src/runner.ts(2 hunks)packages/plugins/tree/src/Main.vue(3 hunks)packages/toolbars/save/src/js/index.ts(2 hunks)
💤 Files with no reviewable changes (2)
- packages/canvas/render/src/render.ts
- packages/canvas/container/src/composables/useMultiSelect.ts
🧰 Additional context used
🧠 Learnings (2)
packages/canvas/container/src/components/CanvasDivider.vue (2)
Learnt from: gene9831
PR: opentiny/tiny-engine#1233
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-03-20T07:20:12.221Z
Learning: In CanvasDivider.vue, even though state.verLeft and state.horizontalTop already include 'px' suffix, the CSS properties in state.dividerStyle still need to append 'px' again according to gene9831, suggesting that these state variables might be processed differently than expected when used in style binding.
Learnt from: gene9831
PR: opentiny/tiny-engine#1226
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-03-19T03:13:51.520Z
Learning: The CSS bug in packages/canvas/container/src/components/CanvasDivider.vue where verLeft already includes "px" but is being appended again in the style object will be fixed in a future update, as confirmed by gene9831.
packages/canvas/container/src/components/CanvasViewerSwitcher.vue (1)
Learnt from: gene9831
PR: opentiny/tiny-engine#1117
File: packages/canvas/container/src/components/CanvasViewerSwitcher.vue:96-117
Timestamp: 2025-02-17T12:11:22.718Z
Learning: In CanvasViewerSwitcher.vue, `state.usedHoverState.element` is guaranteed to have a value when `handleClick` is called, making additional error handling unnecessary.
🧬 Code Graph Analysis (6)
packages/toolbars/save/src/js/index.ts (1)
packages/canvas/container/src/container.ts (1)
canvasApi(823-869)
packages/canvas/container/src/interactions/index.ts (3)
packages/register/src/common.ts (1)
getMergeMeta(179-181)packages/canvas/container/src/interactions/html-interactions.ts (2)
useHoverNode(109-116)useSelectNode(214-223)packages/canvas/container/src/interactions/vue-interactions.ts (2)
useHoverNode(210-217)useSelectNode(241-250)
packages/canvas/container/src/keyboard.ts (5)
packages/canvas/container/src/interactions/index.ts (2)
useSelectNode(50-56)useHoverNode(42-48)packages/canvas/container/src/interactions/html-interactions.ts (2)
useSelectNode(214-223)useHoverNode(109-116)packages/canvas/container/src/interactions/common.ts (2)
selectNodeById(115-126)clearHover(67-69)packages/canvas/container/src/container.ts (1)
removeNodeById(309-319)packages/register/src/hooks.ts (1)
useCanvas(77-77)
packages/canvas/container/src/interactions/common.ts (2)
packages/canvas/container/src/container.ts (2)
getWindow(72-72)querySelectById(334-352)packages/canvas/container/src/interactions/vue-interactions.ts (1)
updateHoverNode(120-128)
packages/canvas/container/src/interactions/html-interactions.ts (5)
packages/canvas/container/src/interactions/common.ts (7)
HoverOrSelectState(9-21)initialHoverState(54-65)clearHover(67-69)getClosedElementHasUid(71-94)getWindowRect(96-105)hoverNodeById(107-113)selectNodeById(115-126)packages/register/src/hooks.ts (1)
useCanvas(77-77)packages/canvas/container/src/container.ts (5)
getConfigure(321-332)canvasState(53-64)getDocument(70-70)scrollToNode(359-384)querySelectById(334-352)packages/canvas/container/src/interactions/vue-interactions.ts (3)
updateHoverNode(120-128)useHoverNode(210-217)useSelectNode(241-250)packages/canvas/container/src/interactions/index.ts (2)
useHoverNode(42-48)useSelectNode(50-56)
packages/canvas/container/src/container.ts (8)
packages/canvas/container/src/interactions/index.ts (2)
useHoverNode(42-48)useSelectNode(50-56)packages/canvas/container/src/interactions/html-interactions.ts (2)
useHoverNode(109-116)useSelectNode(214-223)packages/canvas/container/src/interactions/vue-interactions.ts (2)
useHoverNode(210-217)useSelectNode(241-250)packages/canvas/render/src/render.ts (1)
getController(321-321)packages/register/src/hooks.ts (1)
useCanvas(77-77)packages/canvas/container/src/interactions/common.ts (3)
clearHover(67-69)hoverNodeById(107-113)selectNodeById(115-126)packages/canvas/render/src/material-function/configure.ts (1)
configure(1-1)packages/canvas/types.ts (1)
Node(1-6)
🪛 Biome (1.9.4)
packages/canvas/container/src/interactions/vue-interactions.ts
[error] 118-121: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: push-check
🔇 Additional comments (37)
designer-demo/engine.config.js (1)
6-8: Configuration updated to use Vue-specific interaction hooks.The added configuration properties
dslModeandselectModeset to 'vue' enable the use of Vue-specific interaction hooks for node selection and hover logic in the canvas container. This aligns with the broader refactoring to implement a modular, composable-driven approach for canvas interactions.packages/toolbars/save/src/js/index.ts (2)
73-73: Updated to use new selection API.Correctly destructures
clearSelectfromcanvasApi.valueinstead ofselectNode, aligning with the new composable-based selection management approach.
96-96: Improved selection clearing with optional chaining.Replaces
selectNode(null)withclearSelect?.(), which is more semantic and safely handles potential undefined state.packages/canvas/container/src/components/CanvasDivider.vue (2)
153-154: Updated to use new selection state structure.Correctly destructures layout properties from
selectState.rectinstead of directly fromselectState, aligning with the updated selection state structure.
180-180: Consistent destructuring from nested rect property.Maintains consistency by destructuring layout properties from
props.selectState.rectin thehandleMouseEntermethod.packages/canvas/container/index.ts (2)
2-2: Updated import to use the new interactions module.Replaced the import of
useMultiSelectfrom the composables directory withuseSelectNodefrom the new interactions module, which provides a more flexible approach to selection management.
9-9: Updated exported API to use the new selection composable.The exported
apiobject now providesuseSelectNodeinstead ofuseMultiSelect, representing a breaking change for consumers of this API.Consider documenting this API change to help consumers migrate from
useMultiSelecttouseSelectNode. The new API is part of a broader refactoring to make selection logic more modular and framework-agnostic.packages/plugins/tree/src/Main.vue (3)
41-41: Simplified Vue importsThe imports have been consolidated onto a single line, which improves readability while maintaining the same functionality.
74-75: Selection state management refactorThe code now uses the new
useSelectNodecomposable instead of the previoususeMultiSelectapproach, aligning with the PR's goal of optimizing canvas interaction logic. This change centralizes selection state management.
202-208: Simplified drop handlerThe
handleDropmethod has been refactored to remove the explicit node re-selection after drop operations. This aligns with the centralized selection state management throughuseSelectNodeand reduces redundant operations.packages/canvas/container/src/components/CanvasResizeBorder.vue (2)
124-124: Updated property access patternProperty access has been updated to use the nested
rectobject in the selection state. This change maintains consistency with the new selection state structure introduced in this refactoring.
154-155: Consistent property access patternProperty access has been consistently updated to access geometric properties through
selectState.rectwhile accessingcomponentNamedirectly fromselectState. This maintains a clear separation of concerns and aligns with the new data structure pattern.packages/canvas/render/src/runner.ts (2)
48-59: Added component-to-DOM reference mechanismA new Vue plugin has been introduced that stores component references on DOM nodes, enabling direct access to Vue component instances from DOM elements. This is a key enabler for the Vue-specific interaction logic being implemented.
The plugin correctly handles both the
mountedandupdatedlifecycle hooks to ensure the reference is maintained throughout component updates.
79-79: Applied component reference pluginThe Vue application creation chain now includes the new
GetComponentByDomNodeplugin, ensuring all components in the application have DOM-to-component references.packages/canvas/container/src/components/CanvasRouterJumper.vue (2)
46-52: Simplified hover state monitoringThe watch function has been simplified to only monitor the component name from the hover state, removing duplicate logic related to inactive hover state. This aligns with the new centralized hover state management.
53-60: Updated property access and safer attribute retrievalThe code now accesses geometric properties through
hoverState.rectand uses optional chaining for attribute retrieval, improving robustness by preventing potential null reference errors.packages/canvas/container/src/components/CanvasViewerSwitcher.vue (2)
124-152: Improved hover state validation with centralized logic.The refactoring simplifies hover state handling by removing the dual hover state approach (inactiveHoverState) and implementing more explicit validation logic. The new implementation clearly specifies the conditions for a valid hover state:
- Component must be in the whitelist
- Element must be within an active page container
- Element must not be the active page itself
This aligns with the broader refactoring effort to centralize hover state management using composables.
147-148: Improved usage of React properties.The destructuring of the rect object from state.usedHoverState is more efficient and readable than accessing individual properties separately.
packages/canvas/container/src/keyboard.ts (9)
14-17: Updated imports to use new interaction composables.Replacing
useMultiSelectwithuseHoverNodeanduseSelectNodealigns with the refactoring goal of creating a more modular, composable-driven approach to canvas interactions.
28-31: Updated selection API usage.The function now uses the new
selectNodeByIdmethod from theuseSelectNodecomposable, which provides a more consistent interface for selection operations.
32-37: Consistent selection API usage.Similar to the other handlers, this function has been updated to use the
selectNodeByIdmethod from theuseSelectNodecomposable, ensuring consistency across all selection operations.
39-44: Consistent selection API usage.Updated to use the new selection API, maintaining the same functionality while aligning with the refactored interaction system.
46-51: Consistent selection API usage.Updated to use the new selection API, maintaining the same functionality while aligning with the refactored interaction system.
54-68: Improved delete handler with better hover state management.The refactored delete handler now:
- Iterates over all selected nodes in the collection
- Removes each selected node by ID
- Checks if the removed node was also the hovered node and clears hover state if necessary
- Clears selection after processing all nodes
This approach is more robust and handles edge cases better than before.
116-130: Improved clipboard cut operation.The cut operation now properly:
- Retrieves the current selection state from the composable
- Filters selected nodes to ensure they have valid IDs
- Deep clones the selected nodes to prevent reference issues
- Removes each node by ID
- Clears selection after the operation
This implementation is more explicit and robust.
132-155: Updated clipboard paste operation.The paste operation has been updated to work with the new selection state structure:
- Retrieves the current selection state from the composable
- Gets the last selected item from the array
- Uses the canvas API to get the node and its parent
- Proceeds with insertion as before
This maintains the same functionality while aligning with the new state management approach.
157-173: Updated copy event handler.The copy operation now properly:
- Retrieves the current selection state from the composable
- Filters selected nodes to ensure they have valid IDs
- Deep clones the selected nodes to prevent reference issues
- Validates the nodes before copying them to the clipboard
This maintains the same functionality while aligning with the new state management approach.
packages/canvas/container/src/components/CanvasInsertLine.vue (3)
1-7: Well-structured template for insertion line visualization.The template provides a clear visual indicator for insertion positions with appropriate conditional rendering:
- Only shows when lineState has valid dimensions
- Uses dynamic classes to reflect the insertion position and forbidden status
- Conditionally renders the slot chooser when appropriate
This component helps visually guide users during drag and drop operations.
9-23: Clean component definition with appropriate props.The component defines two props:
lineState: For positioning and state informationhoverState: For hover-related informationThe empty setup function is fine since this is a purely presentational component with no internal state or methods.
25-101: Comprehensive styling with good separation of visual states.The styles provide:
- Clear visual differentiation for different insertion positions (top, left, bottom, right, in)
- Different styling for forbidden states
- Interactive slot chooser with proper hover effects
- Dynamic positioning using v-bind for reactivity
The use of CSS variables for colors supports theming and customization.
packages/canvas/container/src/components/CanvasHover.vue (3)
1-19: Well-designed hover feedback with distinct states.The template provides two types of hover visualizations:
- Active hover with interactive label and container hint
- Inactive hover with non-interactive label
This dual approach gives users clear visual feedback about which elements they can interact with.
21-55: Clean implementation of selection handling.The component properly:
- Imports the required selection API
- Defines the hoverState prop
- Implements a handler for selecting nodes when clicking the label
- Supports both element-based and ID-based selection
- Handles multi-selection with modifier keys (Ctrl/Cmd)
This implementation aligns well with the refactored interaction system.
57-103: Comprehensive styling with good visual hierarchy.The styles provide:
- Clear differentiation between active and inactive hover states
- Proper z-index and positioning
- Transition effects for smooth visual feedback
- Appropriate text styling and positioning for labels
- Dynamic positioning using v-bind for reactivity
The styling is well-organized and uses CSS variables for better maintainability.
packages/canvas/container/src/interactions/index.ts (1)
30-37: Graceful fallback handles unknown modes, butselectModelookup is re-evaluated for every import.
getInteractionFn()queriesgetMergeMeta('engine.config')on each first hook call.
If the user togglesselectModeat runtime (e.g. in a settings panel) the cachedinteractionsFnwill not update, producing incorrect behaviour.Consider:
- Converting
interactionsFnto ashallowRefthat is updated by an explicit watcher on the meta configuration, or- Documenting that
selectModeis read-only at boot and requires a full reload to change.packages/canvas/container/src/interactions/vue-rect.ts (1)
48-66: Initial rect values of0are treated as “unset”.The merge logic uses
!a.topetc. to detect an un-initialised side, but this treats valid0coordinates (element flushed to viewport edge) as “unset”.
A small sentinel value (e.g.Number.POSITIVE_INFINITY/NEGATIVE_INFINITY) or anundefinedinitial side avoids this edge-case.designer-demo/public/mock/bundle.json (1)
14414-14422: Add snippet entry for Date Picker under "Element Plus组件"The new snippet for
ElDatePickerhas been correctly inserted into theElement Plus组件children list with the appropriatename,icon, andsnippetName. The minimalschema: {}is acceptable if you intend to inherit default behavior or prompt configuration at instantiation.packages/canvas/container/src/interactions/vue-interactions.ts (1)
144-168: Promise returned byupdateSelectedNodeis ignored
selectNodeByIdcallsupdateSelectedNodebut does not await the promise.
If callers depend on the selection having scrolled into view (awaiting
scrollToNode), they may run before the async work finishes.Either:
await updateSelectedNode(evt, type, isMultipleSelect)or document clearly that the function is fire-and-forget and callers must not rely on its completion.
| if (instance?.type?.description === 'v-fgt') { | ||
| return getFragmentRect(instance) | ||
| } | ||
|
|
||
| if (instance.el?.nodeType === 1) { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Fragment detection is brittle across Vue versions.
instance.type.description === 'v-fgt' is an internal detail that may change.
Vue exposes the Fragment symbol (import { Fragment } from 'vue') which is more stable:
-import { Fragment } from 'vue'
…
-if (instance?.type?.description === 'v-fgt') {
+if (instance?.type === Fragment) {
return getFragmentRect(instance)
}Switching to the public API avoids breakage when upgrading Vue.
Committable suggestion skipped: line range outside the PR's diff.
| { | ||
| "id": 1, | ||
| "version": "2.4.2", | ||
| "name": { | ||
| "zh_CN": "日期选择器" | ||
| }, | ||
| "component": "ElDatePicker", | ||
| "icon": "datepick", | ||
| "description": "日期选择器", | ||
| "doc_url": "", | ||
| "screenshot": "", | ||
| "tags": "", | ||
| "keywords": "", | ||
| "dev_mode": "proCode", | ||
| "npm": { | ||
| "package": "element-plus", | ||
| "exportName": "ElDatePicker", | ||
| "destructuring": true | ||
| }, | ||
| "group": "表单组件", | ||
| "category": "element-plus", | ||
| "configure": { | ||
| "loop": true, | ||
| "condition": true, | ||
| "styles": true, | ||
| "isContainer": false, | ||
| "isModal": false, | ||
| "isPopper": false, | ||
| "nestingRule": { | ||
| "childWhitelist": "", | ||
| "parentWhitelist": "", | ||
| "descendantBlacklist": "", | ||
| "ancestorWhitelist": "" | ||
| }, | ||
| "isNullNode": false, | ||
| "isLayout": false, | ||
| "rootSelector": "", | ||
| "shortcuts": { | ||
| "properties": ["type", "size"] | ||
| }, | ||
| "contextMenu": { | ||
| "actions": ["copy", "remove", "insert", "updateAttr", "bindEevent", "createBlock"], | ||
| "disable": [] | ||
| }, | ||
| "invalidity": [""], | ||
| "clickCapture": true, | ||
| "framework": "Vue" | ||
| }, | ||
| "schema": { | ||
| "properties": [ | ||
| { | ||
| "name": "0", | ||
| "label": { | ||
| "zh_CN": "基础属性" | ||
| }, | ||
| "content": [ | ||
| { | ||
| "property": "modelValue", | ||
| "label": { | ||
| "text": { | ||
| "zh_CN": "绑定值" | ||
| } | ||
| }, | ||
| "description": { | ||
| "zh_CN": "绑定值" | ||
| }, | ||
| "required": false, | ||
| "readOnly": false, | ||
| "disabled": false, | ||
| "cols": 12, | ||
| "labelPosition": "left", | ||
| "type": "string", | ||
| "widget": { | ||
| "component": "InputConfigurator", | ||
| "props": {} | ||
| } | ||
| }, | ||
| { | ||
| "property": "readonly", | ||
| "label": { | ||
| "text": { | ||
| "zh_CN": "只读" | ||
| } | ||
| }, | ||
| "description": { | ||
| "zh_CN": "是否只读" | ||
| }, | ||
| "required": true, | ||
| "readOnly": false, | ||
| "disabled": false, | ||
| "cols": 12, | ||
| "labelPosition": "left", | ||
| "type": "boolean", | ||
| "defaultValue": false, | ||
| "widget": { | ||
| "component": "CheckBoxConfigurator", | ||
| "props": {} | ||
| } | ||
| }, | ||
| { | ||
| "property": "disabled", | ||
| "label": { | ||
| "text": { | ||
| "zh_CN": "禁用" | ||
| } | ||
| }, | ||
| "description": { | ||
| "zh_CN": "是否禁用" | ||
| }, | ||
| "required": true, | ||
| "readOnly": false, | ||
| "disabled": false, | ||
| "cols": 12, | ||
| "labelPosition": "left", | ||
| "type": "boolean", | ||
| "defaultValue": false, | ||
| "widget": { | ||
| "component": "CheckBoxConfigurator", | ||
| "props": {} | ||
| } | ||
| }, | ||
| { | ||
| "property": "size", | ||
| "label": { | ||
| "text": { | ||
| "zh_CN": "尺寸" | ||
| } | ||
| }, | ||
| "description": { | ||
| "zh_CN": "输入框尺寸" | ||
| }, | ||
| "required": true, | ||
| "readOnly": false, | ||
| "disabled": false, | ||
| "cols": 12, | ||
| "labelPosition": "left", | ||
| "type": "string", | ||
| "defaultValue": "", | ||
| "widget": { | ||
| "component": "SelectConfigurator", | ||
| "props": { | ||
| "allowClear": true, | ||
| "options": [ | ||
| { | ||
| "label": "large", | ||
| "value": "large" | ||
| }, | ||
| { | ||
| "label": "default", | ||
| "value": "default" | ||
| }, | ||
| { | ||
| "label": "small", | ||
| "value": "small" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "property": "editable", | ||
| "label": { | ||
| "text": { | ||
| "zh_CN": "是否可编辑" | ||
| } | ||
| }, | ||
| "description": { | ||
| "zh_CN": "文本框是否可编辑" | ||
| }, | ||
| "required": true, | ||
| "readOnly": false, | ||
| "disabled": false, | ||
| "cols": 12, | ||
| "labelPosition": "left", | ||
| "type": "boolean", | ||
| "defaultValue": true, | ||
| "widget": { | ||
| "component": "CheckBoxConfigurator", | ||
| "props": {} | ||
| }, | ||
| "device": [] | ||
| }, | ||
| { | ||
| "property": "clearable", | ||
| "label": { | ||
| "text": { | ||
| "zh_CN": "是否可清除" | ||
| } | ||
| }, | ||
| "description": { | ||
| "zh_CN": "是否显示清楚按钮" | ||
| }, | ||
| "required": true, | ||
| "readOnly": false, | ||
| "disabled": false, | ||
| "cols": 12, | ||
| "labelPosition": "left", | ||
| "type": "boolean", | ||
| "defaultValue": true, | ||
| "widget": { | ||
| "component": "CheckBoxConfigurator", | ||
| "props": {} | ||
| }, | ||
| "device": [] | ||
| }, | ||
| { | ||
| "property": "placeholder", | ||
| "label": { | ||
| "text": { | ||
| "zh_CN": "占位文本" | ||
| } | ||
| }, | ||
| "description": { | ||
| "zh_CN": "非范围选择时的占位内容" | ||
| }, | ||
| "required": true, | ||
| "readOnly": false, | ||
| "disabled": false, | ||
| "cols": 12, | ||
| "labelPosition": "left", | ||
| "defaultValue": "", | ||
| "type": "string", | ||
| "widget": { | ||
| "component": "InputConfigurator", | ||
| "props": {} | ||
| }, | ||
| "device": [] | ||
| }, | ||
| { | ||
| "property": "start-placeholder", | ||
| "label": { | ||
| "text": { | ||
| "zh_CN": "起始占位文本" | ||
| } | ||
| }, | ||
| "description": { | ||
| "zh_CN": "范围选择时开始日期的占位内容" | ||
| }, | ||
| "required": true, | ||
| "readOnly": false, | ||
| "disabled": false, | ||
| "cols": 12, | ||
| "labelPosition": "left", | ||
| "defaultValue": "", | ||
| "type": "string", | ||
| "widget": { | ||
| "component": "InputConfigurator", | ||
| "props": {} | ||
| }, | ||
| "device": [] | ||
| }, | ||
| { | ||
| "property": "end-placeholder", | ||
| "label": { | ||
| "text": { | ||
| "zh_CN": "结束占位文本" | ||
| } | ||
| }, | ||
| "description": { | ||
| "zh_CN": "范围选择时结束日期的占位内容" | ||
| }, | ||
| "required": true, | ||
| "readOnly": false, | ||
| "disabled": false, | ||
| "cols": 12, | ||
| "labelPosition": "left", | ||
| "defaultValue": "", | ||
| "type": "string", | ||
| "widget": { | ||
| "component": "InputConfigurator", | ||
| "props": {} | ||
| }, | ||
| "device": [] | ||
| }, | ||
| { | ||
| "property": "type", | ||
| "label": { | ||
| "text": { | ||
| "zh_CN": "类型" | ||
| } | ||
| }, | ||
| "description": { | ||
| "zh_CN": "显示类型" | ||
| }, | ||
| "required": true, | ||
| "readOnly": false, | ||
| "disabled": false, | ||
| "cols": 12, | ||
| "labelPosition": "left", | ||
| "defaultValue": "date", | ||
| "type": "string", | ||
| "widget": { | ||
| "component": "SelectConfigurator", | ||
| "props": { | ||
| "options": [ | ||
| { | ||
| "label": "year", | ||
| "value": "year" | ||
| }, | ||
| { | ||
| "label": "years", | ||
| "value": "years" | ||
| }, | ||
| { | ||
| "label": "month", | ||
| "value": "month" | ||
| }, | ||
| { | ||
| "label": "months", | ||
| "value": "months" | ||
| }, | ||
| { | ||
| "label": "date", | ||
| "value": "date" | ||
| }, | ||
| { | ||
| "label": "dates", | ||
| "value": "dates" | ||
| }, | ||
| { | ||
| "label": "datetime", | ||
| "value": "datetime" | ||
| }, | ||
| { | ||
| "label": "week", | ||
| "value": "week" | ||
| }, | ||
| { | ||
| "label": "datetimerange", | ||
| "value": "datetimerange" | ||
| }, | ||
| { | ||
| "label": "daterange", | ||
| "value": "daterange" | ||
| }, | ||
| { | ||
| "label": "monthrange", | ||
| "value": "monthrange" | ||
| }, | ||
| { | ||
| "label": "yearrange", | ||
| "value": "yearrange" | ||
| } | ||
| ] | ||
| } | ||
| }, | ||
| "device": [] | ||
| }, | ||
| { | ||
| "property": "popper-class", | ||
| "label": { | ||
| "text": { | ||
| "zh_CN": "下拉框类名" | ||
| } | ||
| }, | ||
| "description": { | ||
| "zh_CN": "DatePicker 下拉框的类名" | ||
| }, | ||
| "required": true, | ||
| "readOnly": false, | ||
| "disabled": false, | ||
| "cols": 12, | ||
| "labelPosition": "left", | ||
| "defaultValue": "", | ||
| "type": "string", | ||
| "widget": { | ||
| "component": "InputConfigurator", | ||
| "props": {} | ||
| }, | ||
| "device": [] | ||
| } | ||
| ], | ||
| "description": { | ||
| "zh_CN": "" | ||
| } | ||
| } | ||
| ], | ||
| "events": { | ||
| "onUpdate:modelValue": { | ||
| "label": { | ||
| "zh_CN": "双向绑定值改变时触发" | ||
| }, | ||
| "description": { | ||
| "zh_CN": "双向绑定值改变时触发" | ||
| } | ||
| }, | ||
| "onChange": { | ||
| "label": { | ||
| "zh_CN": "用户确认选定的值时触发" | ||
| }, | ||
| "description": { | ||
| "zh_CN": "用户确认选定的值时触发" | ||
| }, | ||
| "type": "event", | ||
| "defaultValue": "" | ||
| }, | ||
| "onBlur": { | ||
| "label": { | ||
| "zh_CN": "在组件 Input 失去焦点时触发" | ||
| }, | ||
| "description": { | ||
| "zh_CN": "在组件 Input 失去焦点时触发" | ||
| }, | ||
| "type": "event", | ||
| "defaultValue": "" | ||
| }, | ||
| "onFocus": { | ||
| "label": { | ||
| "zh_CN": "在组件 Input 获得焦点时触发" | ||
| }, | ||
| "description": { | ||
| "zh_CN": "在组件 Input 获得焦点时触发" | ||
| }, | ||
| "type": "event", | ||
| "defaultValue": "" | ||
| }, | ||
| "onCalendarChange": { | ||
| "label": { | ||
| "zh_CN": "在日历所选日期更改时触发" | ||
| }, | ||
| "description": { | ||
| "zh_CN": "在日历所选日期更改时触发" | ||
| }, | ||
| "type": "event", | ||
| "defaultValue": "" | ||
| }, | ||
| "onPanelChange": { | ||
| "label": { | ||
| "zh_CN": "当日期面板改变时触发。" | ||
| }, | ||
| "description": { | ||
| "zh_CN": "当日期面板改变时触发。" | ||
| }, | ||
| "type": "event", | ||
| "defaultValue": "" | ||
| }, | ||
| "onVisibleChange": { | ||
| "label": { | ||
| "zh_CN": "当 DatePicker 的下拉列表出现/消失时触发" | ||
| }, | ||
| "description": { | ||
| "zh_CN": "当 DatePicker 的下拉列表出现/消失时触发" | ||
| }, | ||
| "type": "event", | ||
| "defaultValue": "" | ||
| } | ||
| }, | ||
| "slots": { | ||
| "default": { | ||
| "label": { | ||
| "zh_CN": "自定义单元格内容" | ||
| }, | ||
| "description": { | ||
| "zh_CN": "自定义单元格内容" | ||
| } | ||
| }, | ||
| "range-separator": { | ||
| "label": { | ||
| "zh_CN": "自定义范围分割符内容" | ||
| }, | ||
| "description": { | ||
| "zh_CN": "自定义范围分割符内容" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }, |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Duplicate component id detected; ensure unique identifiers.
You’ve introduced a new component entry with "id": 1, but immediately afterwards (lines 749–751) there’s another entry also using "id": 1. Component IDs must be unique across bundle.json to prevent collisions in rendering, lookup, or tooling.
Please increment the id to an unused value (e.g., the next highest integer) and re-verify that no other entries share that ID.
Run this script to spot any duplicate IDs in the file:
🏁 Script executed:
#!/usr/bin/env bash
# Detect duplicate `id` values in bundle.json
jq '[.components[].id] | group_by(.)[] | select(length > 1)' designer-demo/public/mock/bundle.jsonLength of output: 192
The initial jq query failed because bundle.json doesn’t have a top-level components array. Let’s extract all "id" fields and then spot any repeats:
🏁 Script executed:
#!/usr/bin/env bash
# Extract every id value from any object and report duplicates
jq -r '.. | objects | select(has("id")) | .id' designer-demo/public/mock/bundle.json \
| sort \
| uniq -c \
| awk '$1 > 1'Length of output: 138
Fix duplicate component IDs in bundle.json
Our scan detected multiple duplicate id values in designer-demo/public/mock/bundle.json:
"1"appears 9 times"2"appears 2 times
Component IDs must be unique to avoid rendering or lookup collisions. Please:
- Update the new entry’s
"id"to an unused integer (e.g., one greater than the current maximum). - Audit and resolve any other duplicates so each component has a unique
id. - Re-run the following to confirm no duplicates remain:
jq -r '.. | objects | select(has("id")) | .id' designer-demo/public/mock/bundle.json \ | sort \ | uniq -c \ | awk '$1 > 1'
| if (selectState.value.length === 1) { | ||
| // TODO: 改成事件通知 | ||
| canvasState.emit('selected', node, parent, type, node?.id) | ||
| } else { | ||
| canvasState.emit('selected', null, null, type, null) | ||
| } |
There was a problem hiding this comment.
Guard canvasState.emit – it is nullable elsewhere
Throughout this repo canvasState.emit is accessed with optional chaining (e.g. line 102).
Lines 169-172 drop the guard; if the emitter is not yet installed this becomes a
runtime error.
- canvasState.emit('selected', node, parent, type, node?.id)
+ canvasState.emit?.('selected', node, parent, type, node?.id)
...
- canvasState.emit('selected', null, null, type, null)
+ canvasState.emit?.('selected', null, null, type, null)Please apply the same defensive style consistently.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (selectState.value.length === 1) { | |
| // TODO: 改成事件通知 | |
| canvasState.emit('selected', node, parent, type, node?.id) | |
| } else { | |
| canvasState.emit('selected', null, null, type, null) | |
| } | |
| if (selectState.value.length === 1) { | |
| // TODO: 改成事件通知 | |
| - canvasState.emit('selected', node, parent, type, node?.id) | |
| + canvasState.emit?.('selected', node, parent, type, node?.id) | |
| } else { | |
| - canvasState.emit('selected', null, null, type, null) | |
| + canvasState.emit?.('selected', null, null, type, null) | |
| } |
| const updateHoverNode = (e: MouseEvent) => { | ||
| const res = getRectAndNode(e) | ||
|
|
||
| if (!res || (res?.node?.id && selectState.value.some((state) => state?.node?.id === res.node.id))) { | ||
| clearHover() | ||
| return | ||
| } | ||
|
|
||
| curHoverState.value = res | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Avoid writing “empty” hover state into the reactive ref
getRectAndNode returns a valid object even when no meaningful node is found (for example when the element has no UID).
Consequently updateHoverNode executes the assignment at line 93, leaving curHoverState with a rectangle {top:0,left:0,width:0,height:0} which the overlay component will still try to render – this produces a “0 × 0” artefact flicker.
- const res = getRectAndNode(e)
-
- if (!res || (res?.node?.id && selectState.value.some((state) => state?.node?.id === res.node.id))) {
- clearHover()
- return
- }
-
- curHoverState.value = res
+ const res = getRectAndNode(e)
+
+ // When `res.node` is null **or** the node is already selected, treat it as “no-hover”.
+ if (!res?.node || selectState.value.some((s) => s?.node?.id === res.node?.id)) {
+ clearHover()
+ return
+ }
+
+ curHoverState.value = resThis prevents the useless write and eliminates overlay flashes.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const updateHoverNode = (e: MouseEvent) => { | |
| const res = getRectAndNode(e) | |
| if (!res || (res?.node?.id && selectState.value.some((state) => state?.node?.id === res.node.id))) { | |
| clearHover() | |
| return | |
| } | |
| curHoverState.value = res | |
| } | |
| const updateHoverNode = (e: MouseEvent) => { | |
| const res = getRectAndNode(e) | |
| // When `res.node` is null **or** the node is already selected, treat it as “no-hover”. | |
| if (!res?.node || selectState.value.some((s) => s?.node?.id === res.node?.id)) { | |
| clearHover() | |
| return | |
| } | |
| curHoverState.value = res | |
| } |
| if (selectState.value.length === 1) { | ||
| canvasState.emit('selected', node, parent, type, node?.id) | ||
| } else { | ||
| canvasState.emit('selected', null, null, type, null) | ||
| } |
There was a problem hiding this comment.
Null-safe emitter call
canvasState.emit might still be null (see earlier defensive usages).
Use optional chaining to prevent runtime crashes:
- canvasState.emit('selected', node, parent, type, node?.id)
+ canvasState.emit?.('selected', node, parent, type, node?.id)
...
- canvasState.emit('selected', null, null, type, null)
+ canvasState.emit?.('selected', null, null, type, null)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (selectState.value.length === 1) { | |
| canvasState.emit('selected', node, parent, type, node?.id) | |
| } else { | |
| canvasState.emit('selected', null, null, type, null) | |
| } | |
| if (selectState.value.length === 1) { | |
| canvasState.emit?.('selected', node, parent, type, node?.id) | |
| } else { | |
| canvasState.emit?.('selected', null, null, type, null) | |
| } |
| if (!element) { | ||
| const { clearHover } = useHoverNode() | ||
|
|
||
| return clearHover() | ||
| } | ||
|
|
There was a problem hiding this comment.
Stale guideline when leaving canvas – add clearLineState
If element is undefined (e.g. pointer exits the iframe), only hover is reset. The last guideline rendered by lineState remains visible.
- if (!element) {
- const { clearHover } = useHoverNode()
- return clearHover()
+ if (!element) {
+ const { clearHover } = useHoverNode()
+ clearHover()
+ clearLineState()
+ return
}Committable suggestion skipped: line range outside the PR's diff.
| export const querySelectById = (id: string) => { | ||
| let selector = `[${NODE_UID}="${id}"]` | ||
| const selector = `[${NODE_UID}="${id}"]` | ||
| const doc = getDocument() | ||
| let element = doc.querySelector(selector) | ||
| const loopId = element?.getAttribute('loop-id') | ||
| if (element && loopId) { | ||
| selector = `[${NODE_UID}="${id}"][${NODE_LOOP}="${loopId}"]` | ||
| element = doc.querySelector(selector) | ||
| const node = useCanvas().getNodeById(id) | ||
| const { rootSelector } = getConfigure(node?.componentName) | ||
|
|
||
| // 根据 id 无法查找到 element,尝试使用 rootSelector 查找 | ||
| if (!element && rootSelector) { | ||
| // TODO: 拖入了多个相同组件的情况下,如何拿到正确的 element | ||
| const newElement = doc.querySelector(rootSelector) | ||
|
|
||
| if (newElement) { | ||
| element = newElement | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
rootSelector fallback may pick the wrong element
When several instances of the same component exist, doc.querySelector(rootSelector) always returns the first match.
This flakes selection/hover for later instances.
-const newElement = doc.querySelector(rootSelector)
-
-if (newElement) {
- element = newElement
-}
+const candidates = Array.from(doc.querySelectorAll(rootSelector))
+element = candidates.find((el) => el.getAttribute(NODE_UID) === id) as Element | undefinedThis keeps behaviour deterministic without changing the public API.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const querySelectById = (id: string) => { | |
| let selector = `[${NODE_UID}="${id}"]` | |
| const selector = `[${NODE_UID}="${id}"]` | |
| const doc = getDocument() | |
| let element = doc.querySelector(selector) | |
| const loopId = element?.getAttribute('loop-id') | |
| if (element && loopId) { | |
| selector = `[${NODE_UID}="${id}"][${NODE_LOOP}="${loopId}"]` | |
| element = doc.querySelector(selector) | |
| const node = useCanvas().getNodeById(id) | |
| const { rootSelector } = getConfigure(node?.componentName) | |
| // 根据 id 无法查找到 element,尝试使用 rootSelector 查找 | |
| if (!element && rootSelector) { | |
| // TODO: 拖入了多个相同组件的情况下,如何拿到正确的 element | |
| const newElement = doc.querySelector(rootSelector) | |
| if (newElement) { | |
| element = newElement | |
| } | |
| } | |
| export const querySelectById = (id: string) => { | |
| const selector = `[${NODE_UID}="${id}"]` | |
| const doc = getDocument() | |
| let element = doc.querySelector(selector) | |
| const node = useCanvas().getNodeById(id) | |
| const { rootSelector } = getConfigure(node?.componentName) | |
| // 根据 id 无法查找到 element,尝试使用 rootSelector 查找 | |
| if (!element && rootSelector) { | |
| // TODO: 拖入了多个相同组件的情况下,如何拿到正确的 element | |
| const candidates = Array.from(doc.querySelectorAll(rootSelector)) | |
| element = candidates.find((el) => el.getAttribute(NODE_UID) === id) as Element | undefined | |
| } | |
| // …rest of the function… | |
| } |
重构: 优化 Vue 画布交互逻辑,提取元素定位和交互方法English | 简体中文
PR
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
Background and solution
What is the current behavior?
Issue Number: N/A
What is the new behavior?
Does this PR introduce a breaking change?
Other information
…tioning and interaction methods
重构: 优化 Vue 画布交互逻辑,提取元素定位和交互方法
Summary by CodeRabbit
New Features
Improvements
Refactor
Bug Fixes
Chores