diff --git a/src/components/modelAlias/ModelMappingDiagram.module.scss b/src/components/modelAlias/ModelMappingDiagram.module.scss index 561a2c2..20c3194 100644 --- a/src/components/modelAlias/ModelMappingDiagram.module.scss +++ b/src/components/modelAlias/ModelMappingDiagram.module.scss @@ -7,6 +7,16 @@ -webkit-overflow-scrolling: touch; } +.tapHint { + position: sticky; + left: 0; + z-index: 3; + font-size: 12px; + color: var(--text-secondary); + padding: 0 4px; + margin-bottom: 8px; +} + .container { display: inline-flex; position: relative; @@ -100,6 +110,12 @@ border-color: var(--primary-color); border-width: 2px; } + + &.selected { + border-color: var(--primary-color); + background-color: var(--bg-secondary); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15); + } } // Mindmap-style provider branch (root node) diff --git a/src/components/modelAlias/ModelMappingDiagram.tsx b/src/components/modelAlias/ModelMappingDiagram.tsx index 57906d9..209a181 100644 --- a/src/components/modelAlias/ModelMappingDiagram.tsx +++ b/src/components/modelAlias/ModelMappingDiagram.tsx @@ -62,6 +62,13 @@ export const ModelMappingDiagram = forwardRef state.resolvedTheme); const isDark = resolvedTheme === 'dark'; + const enableTapLinking = useMemo(() => { + if (typeof window === 'undefined' || typeof window.matchMedia === 'undefined') return false; + return ( + window.matchMedia('(any-pointer: coarse)').matches && + !window.matchMedia('(any-pointer: fine)').matches + ); + }, []); const containerRef = useRef(null); const [lines, setLines] = useState([]); @@ -69,6 +76,8 @@ export const ModelMappingDiagram = forwardRef(null); const [dropTargetAlias, setDropTargetAlias] = useState(null); const [dropTargetSource, setDropTargetSource] = useState(null); + const [tapSourceId, setTapSourceId] = useState(null); + const [tapAlias, setTapAlias] = useState(null); const [extraAliases, setExtraAliases] = useState([]); const [contextMenu, setContextMenu] = useState(null); const [collapsedProviders, setCollapsedProviders] = useState>(new Set()); @@ -301,16 +310,18 @@ export const ModelMappingDiagram = forwardRef Alias const handleDragStart = (e: DragEvent, source: SourceNode) => { + setTapSourceId(null); + setTapAlias(null); setDraggedSource(source); e.dataTransfer.setData('text/plain', source.id); e.dataTransfer.effectAllowed = 'link'; }; const handleDragOver = (e: DragEvent, alias: string) => { + if (!draggedSource || draggedSource.aliases.some((entry) => entry.alias === alias)) return; e.preventDefault(); // Allow drop - if (draggedSource && !draggedSource.aliases.some((entry) => entry.alias === alias)) { - setDropTargetAlias(alias); - } + e.dataTransfer.dropEffect = 'link'; + setDropTargetAlias(alias); }; const handleDragLeave = () => { @@ -328,16 +339,18 @@ export const ModelMappingDiagram = forwardRef Source const handleDragStartAlias = (e: DragEvent, alias: string) => { + setTapSourceId(null); + setTapAlias(null); setDraggedAlias(alias); e.dataTransfer.setData('text/plain', alias); e.dataTransfer.effectAllowed = 'link'; }; const handleDragOverSource = (e: DragEvent, source: SourceNode) => { + if (!draggedAlias || source.aliases.some((entry) => entry.alias === draggedAlias)) return; e.preventDefault(); - if (draggedAlias && !source.aliases.some((entry) => entry.alias === draggedAlias)) { - setDropTargetSource(source.id); - } + e.dataTransfer.dropEffect = 'link'; + setDropTargetSource(source.id); }; const handleDragLeaveSource = () => { @@ -382,6 +395,45 @@ export const ModelMappingDiagram = forwardRef { + if (!onUpdate) return; + if (tapSourceId === source.id) { + setTapSourceId(null); + return; + } + + if (tapAlias) { + onUpdate(source.provider, source.name, tapAlias); + setTapSourceId(null); + setTapAlias(null); + return; + } + + setTapSourceId(source.id); + setTapAlias(null); + }; + + const handleTapSelectAlias = (alias: string) => { + if (!onUpdate) return; + if (tapAlias === alias) { + setTapAlias(null); + return; + } + + if (tapSourceId) { + const source = resolveSourceById(tapSourceId); + if (source) { + onUpdate(source.provider, source.name, alias); + } + setTapSourceId(null); + setTapAlias(null); + return; + } + + setTapAlias(alias); + setTapSourceId(null); + }; + const handleUnlinkSource = (provider: string, sourceModel: string, alias: string) => { if (onDeleteLink) onDeleteLink(provider, sourceModel, alias); }; @@ -459,6 +511,9 @@ export const ModelMappingDiagram = forwardRef + {enableTapLinking && onUpdate && ( +
{t('oauth_model_alias.diagram_tap_hint')}
+ )}
handleContextMenu(e, type, data)} - label={t('oauth_model_alias.diagram_providers')} - expandLabel={t('oauth_model_alias.diagram_expand')} + providerNodes={providerNodes} + collapsedProviders={collapsedProviders} + getProviderColor={getProviderColor} + providerGroupHeights={providerGroupHeights} + providerRefs={providerRefs} + onToggleCollapse={toggleProviderCollapse} + onContextMenu={(e, type, data) => handleContextMenu(e, type, data)} + label={t('oauth_model_alias.diagram_providers')} + expandLabel={t('oauth_model_alias.diagram_expand')} collapseLabel={t('oauth_model_alias.diagram_collapse')} /> { diff --git a/src/components/modelAlias/ModelMappingDiagramColumns.tsx b/src/components/modelAlias/ModelMappingDiagramColumns.tsx index 69e4244..bd8f04b 100644 --- a/src/components/modelAlias/ModelMappingDiagramColumns.tsx +++ b/src/components/modelAlias/ModelMappingDiagramColumns.tsx @@ -85,6 +85,8 @@ interface SourceColumnProps { collapsedProviders: Set; sourceRefs: RefObject>; getProviderColor: (provider: string) => string; + selectedSourceId?: string | null; + onSelectSource?: (source: SourceNode) => void; draggedSource: SourceNode | null; dropTargetSource: string | null; draggable: boolean; @@ -102,6 +104,8 @@ export function SourceColumn({ collapsedProviders, sourceRefs, getProviderColor, + selectedSourceId, + onSelectSource, draggedSource, dropTargetSource, draggable, @@ -134,7 +138,10 @@ export function SourceColumn({ }} className={`${styles.item} ${styles.sourceItem} ${ draggedSource?.id === source.id ? styles.dragging : '' - } ${dropTargetSource === source.id ? styles.dropTarget : ''}`} + } ${dropTargetSource === source.id ? styles.dropTarget : ''} ${ + selectedSourceId === source.id ? styles.selected : '' + }`} + onClick={() => onSelectSource?.(source)} draggable={draggable} onDragStart={(e) => onDragStart(e, source)} onDragEnd={onDragEnd} @@ -169,6 +176,8 @@ interface AliasColumnProps { aliasRefs: RefObject>; dropTargetAlias: string | null; draggedAlias: string | null; + selectedAlias?: string | null; + onSelectAlias?: (alias: string) => void; draggable: boolean; onDragStart: (e: DragEvent, alias: string) => void; onDragEnd: () => void; @@ -184,6 +193,8 @@ export function AliasColumn({ aliasRefs, dropTargetAlias, draggedAlias, + selectedAlias, + onSelectAlias, draggable, onDragStart, onDragEnd, @@ -212,7 +223,10 @@ export function AliasColumn({ }} className={`${styles.item} ${styles.aliasItem} ${ dropTargetAlias === node.alias ? styles.dropTarget : '' - } ${draggedAlias === node.alias ? styles.dragging : ''}`} + } ${draggedAlias === node.alias ? styles.dragging : ''} ${ + selectedAlias === node.alias ? styles.selected : '' + }`} + onClick={() => onSelectAlias?.(node.alias)} draggable={draggable} onDragStart={(e) => onDragStart(e, node.alias)} onDragEnd={onDragEnd} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 82f948e..e6155e3 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -577,6 +577,7 @@ "diagram_settings_title": "Alias settings — {{alias}}", "diagram_settings_source_title": "Source model settings", "diagram_settings_empty": "No mappings for this alias yet.", + "diagram_tap_hint": "On touch devices: tap a source model, then tap an alias to link.", "view_mode": "View mode", "view_mode_diagram": "Diagram", "view_mode_list": "List", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index c748757..10f85c6 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -577,6 +577,7 @@ "diagram_settings_title": "别名设置 — {{alias}}", "diagram_settings_source_title": "源模型设置", "diagram_settings_empty": "该别名暂无映射。", + "diagram_tap_hint": "触摸设备上:先点选源模型,再点选别名即可建立映射。", "view_mode": "视图模式", "view_mode_diagram": "概览", "view_mode_list": "管理",