fix: improve goView media imports and editor context menu parity

This commit is contained in:
ErSan 2026-01-27 20:33:05 +08:00
parent 1bab5905b8
commit 2a5372ecb3
3 changed files with 79 additions and 15 deletions

View File

@ -65,6 +65,23 @@ export function Canvas(props: CanvasProps) {
const selectionAllLocked = selection.length > 0 && selection.every((n) => n.locked);
const selectionAllHidden = selection.length > 0 && selection.every((n) => n.hidden);
const ctxMenuPos = useMemo(() => {
if (!ctx) return null;
// Rough clamp so the menu stays inside the viewport.
// (We don't measure actual size to keep this simple + stable.)
const w = 220;
const h = 320;
const vw = typeof window === 'undefined' ? 10_000 : window.innerWidth;
const vh = typeof window === 'undefined' ? 10_000 : window.innerHeight;
return {
x: Math.max(8, Math.min(ctx.clientX, vw - w - 8)),
y: Math.max(8, Math.min(ctx.clientY, vh - h - 8)),
};
}, [ctx]);
const clientToCanvas = useCallback((clientX: number, clientY: number) => {
const el = ref.current;
if (!el) return null;
@ -175,14 +192,21 @@ export function Canvas(props: CanvasProps) {
const p = clientToWorld(e.clientX, e.clientY);
if (!p) return;
if (targetId && !props.selectionIds.includes(targetId)) {
// goView-ish: right click selects the item.
// Ctrl/Cmd keeps multi-select parity (add to selection instead of replacing).
if ((e as React.MouseEvent).ctrlKey || (e as React.MouseEvent).metaKey) {
props.onToggleSelect(targetId);
} else {
props.onSelectSingle(targetId);
const additive = (e as React.MouseEvent).ctrlKey || (e as React.MouseEvent).metaKey;
if (targetId) {
if (!props.selectionIds.includes(targetId)) {
// goView-ish: right click selects the item.
// Ctrl/Cmd keeps multi-select parity (add to selection instead of replacing).
if (additive) {
props.onToggleSelect(targetId);
} else {
props.onSelectSingle(targetId);
}
}
} else {
// Editor parity: right-click on empty space clears selection (unless additive).
if (!additive) props.onSelectSingle(undefined);
}
setCtx({ clientX: e.clientX, clientY: e.clientY, worldX: p.x, worldY: p.y, targetId });
@ -264,12 +288,12 @@ export function Canvas(props: CanvasProps) {
cursor: props.keyboard.space ? 'grab' : 'default',
}}
>
{ctx && (
{ctx && ctxMenuPos && (
<div
style={{
position: 'fixed',
left: ctx.clientX,
top: ctx.clientY,
left: ctxMenuPos.x,
top: ctxMenuPos.y,
zIndex: 10_000,
background: '#111827',
border: '1px solid rgba(255,255,255,0.12)',

View File

@ -15,6 +15,9 @@ import { convertGoViewTextOptionToNodeProps, type GoViewTextOption } from '../wi
export interface GoViewComponentLike {
id?: string;
// some exports wrap the actual component under a nested field
component?: GoViewComponentLike;
// component identity
key?: string; // e.g. "TextCommon" (sometimes)
componentKey?: string;
@ -58,7 +61,21 @@ export interface GoViewProjectLike {
editCanvasConfig?: GoViewEditCanvasConfigLike;
}
function keyOf(c: GoViewComponentLike): string {
function unwrapComponent(c: GoViewComponentLike): GoViewComponentLike {
// Prefer the nested component shape but keep outer fields as fallback.
// This handles exports like: { id, attr, component: { chartConfig, option } }
const inner = c.component;
if (!inner) return c;
return {
...inner,
...c,
// ensure the nested component doesn't get lost
component: inner.component,
};
}
function keyOf(cIn: GoViewComponentLike): string {
const c = unwrapComponent(cIn);
return (
c.chartConfig?.key ??
c.chartConfig?.chartKey ??
@ -106,7 +123,8 @@ function pick<T>(...values: Array<T | undefined | null>): T | undefined {
return undefined;
}
function optionOf(c: GoViewComponentLike): unknown {
function optionOf(cIn: GoViewComponentLike): unknown {
const c = unwrapComponent(cIn);
const chartData = c.chartConfig?.data as Record<string, unknown> | undefined;
return (
c.option ??
@ -178,7 +196,9 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
const nodes: Array<TextWidgetNode | ImageWidgetNode | IframeWidgetNode | VideoWidgetNode> = [];
for (const c of componentList) {
for (const raw of componentList) {
const c = unwrapComponent(raw);
const rect = c.attr
? {
x: toNumber((c.attr as unknown as Record<string, unknown>).x, 0),

View File

@ -30,7 +30,27 @@ function pickUrlLikeInner(input: unknown, depth: number): string {
const obj = input as Record<string, unknown>;
// Common direct keys.
for (const key of ['value', 'url', 'src', 'href', 'link', 'path', 'iframeUrl', 'videoUrl', 'mp4']) {
// Keep this list generous; imports come from many low-code editors.
for (const key of [
'value',
'url',
'src',
'href',
'link',
'path',
'source',
'address',
// iframe-ish
'iframeUrl',
'iframeSrc',
'embedUrl',
// video-ish
'videoUrl',
'videoSrc',
'mp4',
'm3u8',
'flv',
]) {
const v = obj[key];
if (typeof v === 'string' && v) return v;
}
@ -38,7 +58,7 @@ function pickUrlLikeInner(input: unknown, depth: number): string {
if (depth <= 0) return '';
// Common nesting keys.
for (const key of ['dataset', 'data', 'config', 'option', 'options', 'props']) {
for (const key of ['dataset', 'data', 'config', 'option', 'options', 'props', 'source', 'media']) {
const v = obj[key];
const nested = pickUrlLikeInner(v, depth - 1);
if (nested) return nested;