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;
}
.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)

View File

@@ -62,6 +62,13 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
const { t } = useTranslation();
const resolvedTheme = useThemeStore((state) => 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<HTMLDivElement>(null);
const [lines, setLines] = useState<DiagramLine[]>([]);
@@ -69,6 +76,8 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
const [draggedAlias, setDraggedAlias] = useState<string | null>(null);
const [dropTargetAlias, setDropTargetAlias] = 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 [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
const [collapsedProviders, setCollapsedProviders] = useState<Set<string>>(new Set());
@@ -301,16 +310,18 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
// Drag and Drop handlers
// 1. Source -> 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<ModelMappingDiagramRef, ModelMappi
// 2. Alias -> 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<ModelMappingDiagramRef, ModelMappi
[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) => {
if (onDeleteLink) onDeleteLink(provider, sourceModel, alias);
};
@@ -459,6 +511,9 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
return (
<div className={[styles.scrollContainer, className].filter(Boolean).join(' ')}>
{enableTapLinking && onUpdate && (
<div className={styles.tapHint}>{t('oauth_model_alias.diagram_tap_hint')}</div>
)}
<div
className={styles.container}
ref={containerRef}
@@ -480,15 +535,15 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
</svg>
<ProviderColumn
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')}
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')}
/>
<SourceColumn
@@ -496,6 +551,8 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
collapsedProviders={collapsedProviders}
sourceRefs={sourceRefs}
getProviderColor={getProviderColor}
selectedSourceId={enableTapLinking ? tapSourceId : null}
onSelectSource={enableTapLinking ? handleTapSelectSource : undefined}
draggedSource={draggedSource}
dropTargetSource={dropTargetSource}
draggable={!!onUpdate}
@@ -515,6 +572,8 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
aliasRefs={aliasRefs}
dropTargetAlias={dropTargetAlias}
draggedAlias={draggedAlias}
selectedAlias={enableTapLinking ? tapAlias : null}
onSelectAlias={enableTapLinking ? handleTapSelectAlias : undefined}
draggable={!!onUpdate}
onDragStart={handleDragStartAlias}
onDragEnd={() => {

View File

@@ -85,6 +85,8 @@ interface SourceColumnProps {
collapsedProviders: Set<string>;
sourceRefs: RefObject<Map<string, HTMLDivElement>>;
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<Map<string, HTMLDivElement>>;
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}

View File

@@ -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",

View File

@@ -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": "管理",