fix(model-alias): improve diagram mobile layout and refresh reliability

This commit is contained in:
LTbinglingfeng
2026-02-05 00:55:03 +08:00
parent 9887a78889
commit 5241d52b14
3 changed files with 77 additions and 33 deletions

View File

@@ -17,8 +17,8 @@
user-select: none; user-select: none;
@media (max-width: 768px) { @media (max-width: 768px) {
justify-content: flex-start; // Give mobile extra horizontal room to reduce line overlap; users can swipe to scroll.
gap: 16px; min-width: max(100%, 960px);
padding: 12px 0; padding: 12px 0;
} }
} }
@@ -158,6 +158,13 @@
} }
} }
.providerGroup {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
}
.sourceItem, .sourceItem,
.aliasItem { .aliasItem {
cursor: grab; cursor: grab;

View File

@@ -72,6 +72,7 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
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());
const [providerGroupHeights, setProviderGroupHeights] = useState<Record<string, number>>({});
const [renameState, setRenameState] = useState<{ oldAlias: string } | null>(null); const [renameState, setRenameState] = useState<{ oldAlias: string } | null>(null);
const [renameValue, setRenameValue] = useState(''); const [renameValue, setRenameValue] = useState('');
const [renameError, setRenameError] = useState(''); const [renameError, setRenameError] = useState('');
@@ -179,6 +180,7 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
if (!containerRef.current) return; if (!containerRef.current) return;
const containerRect = containerRef.current.getBoundingClientRect(); const containerRect = containerRef.current.getBoundingClientRect();
const newLines: { path: string; color: string; id: string }[] = []; const newLines: { path: string; color: string; id: string }[] = [];
const nextProviderGroupHeights: Record<string, number> = {};
const bezier = ( const bezier = (
x1: number, y1: number, x1: number, y1: number,
@@ -193,6 +195,15 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
const collapsed = collapsedProviders.has(provider); const collapsed = collapsedProviders.has(provider);
if (collapsed) return; if (collapsed) return;
if (sources.length > 0) {
const firstEl = sourceRefs.current.get(sources[0].id);
const lastEl = sourceRefs.current.get(sources[sources.length - 1].id);
if (firstEl && lastEl) {
const height = Math.max(0, Math.round(lastEl.getBoundingClientRect().bottom - firstEl.getBoundingClientRect().top));
if (height > 0) nextProviderGroupHeights[provider] = height;
}
}
const providerEl = providerRefs.current.get(provider); const providerEl = providerRefs.current.get(provider);
if (!providerEl) return; if (!providerEl) return;
const providerRect = providerEl.getBoundingClientRect(); const providerRect = providerEl.getBoundingClientRect();
@@ -241,6 +252,17 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
}); });
setLines(newLines); setLines(newLines);
setProviderGroupHeights((prev) => {
const prevKeys = Object.keys(prev);
const nextKeys = Object.keys(nextProviderGroupHeights);
if (prevKeys.length !== nextKeys.length) return nextProviderGroupHeights;
for (const key of nextKeys) {
if (!(key in prev) || prev[key] !== nextProviderGroupHeights[key]) {
return nextProviderGroupHeights;
}
}
return prev;
});
}, [providerNodes, collapsedProviders]); }, [providerNodes, collapsedProviders]);
useImperativeHandle( useImperativeHandle(
@@ -263,6 +285,12 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
}; };
}, [updateLines, aliasNodes]); }, [updateLines, aliasNodes]);
useLayoutEffect(() => {
updateLines();
const raf = requestAnimationFrame(updateLines);
return () => cancelAnimationFrame(raf);
}, [providerGroupHeights, updateLines]);
useEffect(() => { useEffect(() => {
if (!containerRef.current || typeof ResizeObserver === 'undefined') return; if (!containerRef.current || typeof ResizeObserver === 'undefined') return;
const observer = new ResizeObserver(() => updateLines()); const observer = new ResizeObserver(() => updateLines());
@@ -452,14 +480,15 @@ export const ModelMappingDiagram = forwardRef<ModelMappingDiagramRef, ModelMappi
</svg> </svg>
<ProviderColumn <ProviderColumn
providerNodes={providerNodes} providerNodes={providerNodes}
collapsedProviders={collapsedProviders} collapsedProviders={collapsedProviders}
getProviderColor={getProviderColor} getProviderColor={getProviderColor}
providerRefs={providerRefs} providerGroupHeights={providerGroupHeights}
onToggleCollapse={toggleProviderCollapse} providerRefs={providerRefs}
onContextMenu={(e, type, data) => handleContextMenu(e, type, data)} onToggleCollapse={toggleProviderCollapse}
label={t('oauth_model_alias.diagram_providers')} onContextMenu={(e, type, data) => handleContextMenu(e, type, data)}
expandLabel={t('oauth_model_alias.diagram_expand')} label={t('oauth_model_alias.diagram_providers')}
expandLabel={t('oauth_model_alias.diagram_expand')}
collapseLabel={t('oauth_model_alias.diagram_collapse')} collapseLabel={t('oauth_model_alias.diagram_collapse')}
/> />
<SourceColumn <SourceColumn

View File

@@ -6,6 +6,7 @@ interface ProviderColumnProps {
providerNodes: ProviderNode[]; providerNodes: ProviderNode[];
collapsedProviders: Set<string>; collapsedProviders: Set<string>;
getProviderColor: (provider: string) => string; getProviderColor: (provider: string) => string;
providerGroupHeights?: Record<string, number>;
providerRefs: RefObject<Map<string, HTMLDivElement>>; providerRefs: RefObject<Map<string, HTMLDivElement>>;
onToggleCollapse: (provider: string) => void; onToggleCollapse: (provider: string) => void;
onContextMenu: (e: ReactMouseEvent, type: 'provider' | 'background', data?: string) => void; onContextMenu: (e: ReactMouseEvent, type: 'provider' | 'background', data?: string) => void;
@@ -18,6 +19,7 @@ export function ProviderColumn({
providerNodes, providerNodes,
collapsedProviders, collapsedProviders,
getProviderColor, getProviderColor,
providerGroupHeights = {},
providerRefs, providerRefs,
onToggleCollapse, onToggleCollapse,
onContextMenu, onContextMenu,
@@ -37,34 +39,40 @@ export function ProviderColumn({
<div className={styles.columnHeader}>{label}</div> <div className={styles.columnHeader}>{label}</div>
{providerNodes.map(({ provider, sources }) => { {providerNodes.map(({ provider, sources }) => {
const collapsed = collapsedProviders.has(provider); const collapsed = collapsedProviders.has(provider);
const groupHeight = collapsed ? undefined : providerGroupHeights[provider];
return ( return (
<div <div
key={provider} key={provider}
ref={(el) => { className={styles.providerGroup}
if (el) providerRefs.current?.set(provider, el); style={groupHeight ? { height: groupHeight } : undefined}
else providerRefs.current?.delete(provider);
}}
className={`${styles.item} ${styles.providerItem}`}
style={{ borderLeftColor: getProviderColor(provider) }}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
onContextMenu(e, 'provider', provider);
}}
> >
<button <div
type="button" ref={(el) => {
className={styles.collapseBtn} if (el) providerRefs.current?.set(provider, el);
onClick={() => onToggleCollapse(provider)} else providerRefs.current?.delete(provider);
aria-label={collapsed ? expandLabel : collapseLabel} }}
title={collapsed ? expandLabel : collapseLabel} className={`${styles.item} ${styles.providerItem}`}
style={{ borderLeftColor: getProviderColor(provider) }}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
onContextMenu(e, 'provider', provider);
}}
> >
<span className={collapsed ? styles.chevronRight : styles.chevronDown} /> <button
</button> type="button"
<span className={styles.providerLabel} style={{ color: getProviderColor(provider) }}> className={styles.collapseBtn}
{provider} onClick={() => onToggleCollapse(provider)}
</span> aria-label={collapsed ? expandLabel : collapseLabel}
<span className={styles.itemCount}>{sources.length}</span> title={collapsed ? expandLabel : collapseLabel}
>
<span className={collapsed ? styles.chevronRight : styles.chevronDown} />
</button>
<span className={styles.providerLabel} style={{ color: getProviderColor(provider) }}>
{provider}
</span>
<span className={styles.itemCount}>{sources.length}</span>
</div>
</div> </div>
); );
})} })}