fix(model-alias): restore diagram drag-and-drop and add touch tap-to-link fallback

This commit is contained in:
LTbinglingfeng
2026-02-05 01:26:01 +08:00
parent 5241d52b14
commit d4bc0bc622
5 changed files with 108 additions and 17 deletions

View File

@@ -7,6 +7,16 @@
-webkit-overflow-scrolling: touch; -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 { .container {
display: inline-flex; display: inline-flex;
position: relative; position: relative;
@@ -100,6 +110,12 @@
border-color: var(--primary-color); border-color: var(--primary-color);
border-width: 2px; 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) // Mindmap-style provider branch (root node)

View File

@@ -62,6 +62,13 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
const { t } = useTranslation(); const { t } = useTranslation();
const resolvedTheme = useThemeStore((state) => state.resolvedTheme); const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
const isDark = resolvedTheme === 'dark'; 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<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [lines, setLines] = useState<DiagramLine[]>([]); const [lines, setLines] = useState<DiagramLine[]>([]);
@@ -69,6 +76,8 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
const [draggedAlias, setDraggedAlias] = useState<string | null>(null); const [draggedAlias, setDraggedAlias] = useState<string | null>(null);
const [dropTargetAlias, setDropTargetAlias] = useState<string | null>(null); const [dropTargetAlias, setDropTargetAlias] = useState<string | null>(null);
const [dropTargetSource, setDropTargetSource] = useState<string | null>(null); const [dropTargetSource, setDropTargetSource] = useState<string | null>(null);
const [tapSourceId, setTapSourceId] = useState<string | null>(null);
const [tapAlias, setTapAlias] = useState<string | null>(null);
const [extraAliases, setExtraAliases] = useState<string[]>([]); const [extraAliases, setExtraAliases] = useState<string[]>([]);
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null); const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
const [collapsedProviders, setCollapsedProviders] = useState<Set<string>>(new Set()); const [collapsedProviders, setCollapsedProviders] = useState<Set<string>>(new Set());
@@ -301,16 +310,18 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
// Drag and Drop handlers // Drag and Drop handlers
// 1. Source -> Alias // 1. Source -> Alias
const handleDragStart = (e: DragEvent, source: SourceNode) => { const handleDragStart = (e: DragEvent, source: SourceNode) => {
setTapSourceId(null);
setTapAlias(null);
setDraggedSource(source); setDraggedSource(source);
e.dataTransfer.setData('text/plain', source.id); e.dataTransfer.setData('text/plain', source.id);
e.dataTransfer.effectAllowed = 'link'; e.dataTransfer.effectAllowed = 'link';
}; };
const handleDragOver = (e: DragEvent, alias: string) => { const handleDragOver = (e: DragEvent, alias: string) => {
if (!draggedSource || draggedSource.aliases.some((entry) => entry.alias === alias)) return;
e.preventDefault(); // Allow drop e.preventDefault(); // Allow drop
if (draggedSource && !draggedSource.aliases.some((entry) => entry.alias === alias)) { e.dataTransfer.dropEffect = 'link';
setDropTargetAlias(alias); setDropTargetAlias(alias);
}
}; };
const handleDragLeave = () => { const handleDragLeave = () => {
@@ -328,16 +339,18 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
// 2. Alias -> Source // 2. Alias -> Source
const handleDragStartAlias = (e: DragEvent, alias: string) => { const handleDragStartAlias = (e: DragEvent, alias: string) => {
setTapSourceId(null);
setTapAlias(null);
setDraggedAlias(alias); setDraggedAlias(alias);
e.dataTransfer.setData('text/plain', alias); e.dataTransfer.setData('text/plain', alias);
e.dataTransfer.effectAllowed = 'link'; e.dataTransfer.effectAllowed = 'link';
}; };
const handleDragOverSource = (e: DragEvent, source: SourceNode) => { const handleDragOverSource = (e: DragEvent, source: SourceNode) => {
if (!draggedAlias || source.aliases.some((entry) => entry.alias === draggedAlias)) return;
e.preventDefault(); e.preventDefault();
if (draggedAlias && !source.aliases.some((entry) => entry.alias === draggedAlias)) { e.dataTransfer.dropEffect = 'link';
setDropTargetSource(source.id); setDropTargetSource(source.id);
}
}; };
const handleDragLeaveSource = () => { const handleDragLeaveSource = () => {
@@ -382,6 +395,45 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
[providerNodes] [providerNodes]
); );
const handleTapSelectSource = (source: SourceNode) => {
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) => { const handleUnlinkSource = (provider: string, sourceModel: string, alias: string) => {
if (onDeleteLink) onDeleteLink(provider, sourceModel, alias); if (onDeleteLink) onDeleteLink(provider, sourceModel, alias);
}; };
@@ -459,6 +511,9 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
return ( return (
<div className={[styles.scrollContainer, className].filter(Boolean).join(' ')}> <div className={[styles.scrollContainer, className].filter(Boolean).join(' ')}>
{enableTapLinking && onUpdate && (
<div className={styles.tapHint}>{t('oauth_model_alias.diagram_tap_hint')}</div>
)}
<div <div
className={styles.container} className={styles.container}
ref={containerRef} ref={containerRef}
@@ -480,15 +535,15 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
</svg> </svg>
<ProviderColumn <ProviderColumn
providerNodes={providerNodes} providerNodes={providerNodes}
collapsedProviders={collapsedProviders} collapsedProviders={collapsedProviders}
getProviderColor={getProviderColor} getProviderColor={getProviderColor}
providerGroupHeights={providerGroupHeights} providerGroupHeights={providerGroupHeights}
providerRefs={providerRefs} providerRefs={providerRefs}
onToggleCollapse={toggleProviderCollapse} onToggleCollapse={toggleProviderCollapse}
onContextMenu={(e, type, data) => handleContextMenu(e, type, data)} onContextMenu={(e, type, data) => handleContextMenu(e, type, data)}
label={t('oauth_model_alias.diagram_providers')} label={t('oauth_model_alias.diagram_providers')}
expandLabel={t('oauth_model_alias.diagram_expand')} expandLabel={t('oauth_model_alias.diagram_expand')}
collapseLabel={t('oauth_model_alias.diagram_collapse')} collapseLabel={t('oauth_model_alias.diagram_collapse')}
/> />
<SourceColumn <SourceColumn
@@ -496,6 +551,8 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
collapsedProviders={collapsedProviders} collapsedProviders={collapsedProviders}
sourceRefs={sourceRefs} sourceRefs={sourceRefs}
getProviderColor={getProviderColor} getProviderColor={getProviderColor}
selectedSourceId={enableTapLinking ? tapSourceId : null}
onSelectSource={enableTapLinking ? handleTapSelectSource : undefined}
draggedSource={draggedSource} draggedSource={draggedSource}
dropTargetSource={dropTargetSource} dropTargetSource={dropTargetSource}
draggable={!!onUpdate} draggable={!!onUpdate}
@@ -515,6 +572,8 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
aliasRefs={aliasRefs} aliasRefs={aliasRefs}
dropTargetAlias={dropTargetAlias} dropTargetAlias={dropTargetAlias}
draggedAlias={draggedAlias} draggedAlias={draggedAlias}
selectedAlias={enableTapLinking ? tapAlias : null}
onSelectAlias={enableTapLinking ? handleTapSelectAlias : undefined}
draggable={!!onUpdate} draggable={!!onUpdate}
onDragStart={handleDragStartAlias} onDragStart={handleDragStartAlias}
onDragEnd={() => { onDragEnd={() => {

View File

@@ -85,6 +85,8 @@ interface SourceColumnProps {
collapsedProviders: Set<string>; collapsedProviders: Set<string>;
sourceRefs: RefObject<Map<string, HTMLDivElement>>; sourceRefs: RefObject<Map<string, HTMLDivElement>>;
getProviderColor: (provider: string) => string; getProviderColor: (provider: string) => string;
selectedSourceId?: string | null;
onSelectSource?: (source: SourceNode) => void;
draggedSource: SourceNode | null; draggedSource: SourceNode | null;
dropTargetSource: string | null; dropTargetSource: string | null;
draggable: boolean; draggable: boolean;
@@ -102,6 +104,8 @@ export function SourceColumn({
collapsedProviders, collapsedProviders,
sourceRefs, sourceRefs,
getProviderColor, getProviderColor,
selectedSourceId,
onSelectSource,
draggedSource, draggedSource,
dropTargetSource, dropTargetSource,
draggable, draggable,
@@ -134,7 +138,10 @@ export function SourceColumn({
}} }}
className={`${styles.item} ${styles.sourceItem} ${ className={`${styles.item} ${styles.sourceItem} ${
draggedSource?.id === source.id ? styles.dragging : '' 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} draggable={draggable}
onDragStart={(e) => onDragStart(e, source)} onDragStart={(e) => onDragStart(e, source)}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
@@ -169,6 +176,8 @@ interface AliasColumnProps {
aliasRefs: RefObject<Map<string, HTMLDivElement>>; aliasRefs: RefObject<Map<string, HTMLDivElement>>;
dropTargetAlias: string | null; dropTargetAlias: string | null;
draggedAlias: string | null; draggedAlias: string | null;
selectedAlias?: string | null;
onSelectAlias?: (alias: string) => void;
draggable: boolean; draggable: boolean;
onDragStart: (e: DragEvent, alias: string) => void; onDragStart: (e: DragEvent, alias: string) => void;
onDragEnd: () => void; onDragEnd: () => void;
@@ -184,6 +193,8 @@ export function AliasColumn({
aliasRefs, aliasRefs,
dropTargetAlias, dropTargetAlias,
draggedAlias, draggedAlias,
selectedAlias,
onSelectAlias,
draggable, draggable,
onDragStart, onDragStart,
onDragEnd, onDragEnd,
@@ -212,7 +223,10 @@ export function AliasColumn({
}} }}
className={`${styles.item} ${styles.aliasItem} ${ className={`${styles.item} ${styles.aliasItem} ${
dropTargetAlias === node.alias ? styles.dropTarget : '' 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} draggable={draggable}
onDragStart={(e) => onDragStart(e, node.alias)} onDragStart={(e) => onDragStart(e, node.alias)}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}

View File

@@ -577,6 +577,7 @@
"diagram_settings_title": "Alias settings — {{alias}}", "diagram_settings_title": "Alias settings — {{alias}}",
"diagram_settings_source_title": "Source model settings", "diagram_settings_source_title": "Source model settings",
"diagram_settings_empty": "No mappings for this alias yet.", "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": "View mode",
"view_mode_diagram": "Diagram", "view_mode_diagram": "Diagram",
"view_mode_list": "List", "view_mode_list": "List",

View File

@@ -577,6 +577,7 @@
"diagram_settings_title": "别名设置 — {{alias}}", "diagram_settings_title": "别名设置 — {{alias}}",
"diagram_settings_source_title": "源模型设置", "diagram_settings_source_title": "源模型设置",
"diagram_settings_empty": "该别名暂无映射。", "diagram_settings_empty": "该别名暂无映射。",
"diagram_tap_hint": "触摸设备上:先点选源模型,再点选别名即可建立映射。",
"view_mode": "视图模式", "view_mode": "视图模式",
"view_mode_diagram": "概览", "view_mode_diagram": "概览",
"view_mode_list": "管理", "view_mode_list": "管理",