fix(diagram): display relationship cardinalities

This commit is contained in:
zipg authored and GitHub committed 2026-08-02 16:34:35 +08:00
1 parent 86c53479a1
commit fb7b2878fd
12 files changed
+316 -28

No files matched your search

@@ -14,7 +14,7 @@ import { DIAGRAM_SQL_TYPES, isSchemaAware as isSchemaAwareDatabase } from "@/lib
import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions";
import { buildDiagramJoinSql, buildDiagramRelationships, filterDiagramTables, layoutDiagramTables, normalizeCustomDiagramRelationship, type CustomDiagramRelationship, type DiagramPosition, type DiagramRelationship, type DiagramTable } from "@/lib/diagram/erDiagram";
import { buildEngineeringDiagram } from "@/lib/diagram/engineeringDiagram";
import { buildEngineeringDiagramSvg, buildTableDiagramSvg, diagramSvgFileName } from "@/lib/export/diagramSvgExport";
import { buildEngineeringDiagramSvg, buildTableDiagramSvg, diagramSvgFileName, type TableDiagramRelationshipLayout } from "@/lib/export/diagramSvgExport";
import { clampDiagramZoom, zoomFromGestureScale, zoomFromWheelDelta } from "@/lib/diagram/diagramZoom";
import { Copy, Download, KeyRound, Link2, Loader2, Maximize2, Network, Plus, RefreshCw, Search, Table2, Trash2, X, ZoomIn, ZoomOut } from "@lucide/vue";
import { useToast } from "@/composables/useToast";
@@ -88,7 +88,7 @@ const relationshipDraft = ref({
sourceColumn: "",
targetTable: "",
targetColumn: "",
cardinality: "one-to-many" as "one-to-one" | "one-to-many" | "many-to-one",
cardinality: "one-to-many" as "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many",
});
const sqlConnections = computed(() => store.connections.filter((connection) => DIAGRAM_SQL_TYPES.has(connection.db_type)));
@@ -253,6 +253,7 @@ function defaultRelationshipName(relationship: Omit<CustomDiagramRelationship, "
function relationshipCardinality(): Pick<CustomDiagramRelationship, "sourceCardinality" | "targetCardinality"> {
if (relationshipDraft.value.cardinality === "one-to-one") return { sourceCardinality: "1", targetCardinality: "1" };
if (relationshipDraft.value.cardinality === "many-to-one") return { sourceCardinality: "N", targetCardinality: "1" };
if (relationshipDraft.value.cardinality === "many-to-many") return { sourceCardinality: "N", targetCardinality: "N" };
return { sourceCardinality: "1", targetCardinality: "N" };
}
@@ -423,10 +424,17 @@ function candidateRouteXs(source: TableRect, target: TableRect): number[] {
});
}
function relationshipPath(relationship: DiagramRelationship): string {
function relationshipLayout(relationship: DiagramRelationship): TableDiagramRelationshipLayout {
const source = getTableRect(relationship.sourceTable);
const target = getTableRect(relationship.targetTable);
if (!source || !target) return "";
if (!source || !target) {
return {
path: "",
routePoints: [],
sourceCardinality: { x: 0, y: 0 },
targetCardinality: { x: 0, y: 0 },
};
}
const y1 = columnAnchorY(relationship.sourceTable, relationship.sourceColumn);
const y2 = columnAnchorY(relationship.targetTable, relationship.targetColumn);
@@ -444,9 +452,29 @@ function relationshipPath(relationship: DiagramRelationship): string {
const x1 = routeSideX(source, routeX, 2);
const x2 = routeSideX(target, routeX, 2);
return `M ${x1} ${y1} L ${routeX} ${y1} L ${routeX} ${y2} L ${x2} ${y2}`;
const cardinalityOffset = 14;
const cardinalityLift = 10;
return {
path: `M ${x1} ${y1} L ${routeX} ${y1} L ${routeX} ${y2} L ${x2} ${y2}`,
routePoints: [
{ x: x1, y: y1 },
{ x: routeX, y: y1 },
{ x: routeX, y: y2 },
{ x: x2, y: y2 },
],
sourceCardinality: {
x: x1 + (routeX < source.x ? -cardinalityOffset : cardinalityOffset),
y: y1 - cardinalityLift,
},
targetCardinality: {
x: x2 + (routeX < target.x ? -cardinalityOffset : cardinalityOffset),
y: y2 - cardinalityLift,
},
};
}
const tableRelationshipLayouts = computed<Record<string, TableDiagramRelationshipLayout>>(() => Object.fromEntries(visibleRelationships.value.map((relationship) => [relationship.id, relationshipLayout(relationship)])));
function engineeringEntityCenter(tableName: string): DiagramPosition {
const entity = engineeringDiagram.value.entities.find((item) => item.name === tableName);
return entity ? { x: entity.x + entity.width / 2, y: entity.y + entity.height / 2 } : { x: 0, y: 0 };
@@ -541,8 +569,12 @@ async function setSchema(value: string) {
async function loadTableDiagramData(tableName: string, querySchema: string): Promise<DiagramTable> {
try {
const [columns, foreignKeys] = await Promise.all([api.getColumns(connectionId.value, database.value, querySchema, tableName), api.listForeignKeys(connectionId.value, database.value, querySchema, tableName).catch(() => [])]);
return { name: tableName, columns, foreignKeys };
const [columns, foreignKeys, indexes] = await Promise.all([
api.getColumns(connectionId.value, database.value, querySchema, tableName),
api.listForeignKeys(connectionId.value, database.value, querySchema, tableName).catch(() => []),
api.listIndexes(connectionId.value, database.value, querySchema, tableName).catch(() => []),
]);
return { name: tableName, columns, foreignKeys, indexes };
} catch (e) {
failedTableCount.value += 1;
console.warn(`[diagram] failed to load table metadata: ${tableName}`, e);
@@ -655,10 +687,6 @@ function resetZoomAndLayout() {
resetLayout();
}
function tableRelationshipPaths(): Record<string, string> {
return Object.fromEntries(visibleRelationships.value.map((relationship) => [relationship.id, relationshipPath(relationship)]));
}
function currentDiagramSvg(): string {
if (diagramMode.value === "engineering") {
return buildEngineeringDiagramSvg(engineeringDiagram.value);
@@ -668,7 +696,7 @@ function currentDiagramSvg(): string {
tables: visibleTables.value,
relationships: visibleRelationships.value,
positions: positions.value,
relationshipPaths: tableRelationshipPaths(),
relationshipLayouts: tableRelationshipLayouts.value,
canvas: canvasSize.value,
cardWidth: CARD_WIDTH,
cardHeaderHeight: CARD_HEADER_HEIGHT,
@@ -941,6 +969,7 @@ onUnmounted(stopDrag);
<SelectContent>
<SelectItem value="one-to-many">{{ t("diagram.cardinalityOneToMany") }}</SelectItem>
<SelectItem value="many-to-one">{{ t("diagram.cardinalityManyToOne") }}</SelectItem>
<SelectItem value="many-to-many">{{ t("diagram.cardinalityManyToMany") }}</SelectItem>
<SelectItem value="one-to-one">{{ t("diagram.cardinalityOneToOne") }}</SelectItem>
</SelectContent>
</Select>
@@ -1026,9 +1055,39 @@ onUnmounted(stopDrag);
<path d="M 0 0 L 8 4 L 0 8 z" class="fill-primary/70" />
</marker>
</defs>
<path v-for="relationship in visibleRelationships" :key="relationship.id" :d="relationshipPath(relationship)" class="fill-none stroke-primary/55" stroke-width="1.6" marker-end="url(#diagram-arrow)">
<title>{{ relationshipTitle(relationship) }}</title>
</path>
<template v-for="relationship in visibleRelationships" :key="relationship.id">
<path :d="tableRelationshipLayouts[relationship.id]?.path" class="fill-none stroke-primary/55" stroke-width="1.6" marker-end="url(#diagram-arrow)">
<title>{{ relationshipTitle(relationship) }}</title>
</path>
<text
:data-relationship-id="relationship.id"
data-cardinality-end="source"
:x="tableRelationshipLayouts[relationship.id]?.sourceCardinality.x"
:y="tableRelationshipLayouts[relationship.id]?.sourceCardinality.y"
text-anchor="middle"
dominant-baseline="middle"
paint-order="stroke"
stroke-width="4"
stroke-linejoin="round"
class="fill-foreground stroke-background text-[12px] font-semibold"
>
{{ relationship.sourceCardinality }}
</text>
<text
:data-relationship-id="relationship.id"
data-cardinality-end="target"
:x="tableRelationshipLayouts[relationship.id]?.targetCardinality.x"
:y="tableRelationshipLayouts[relationship.id]?.targetCardinality.y"
text-anchor="middle"
dominant-baseline="middle"
paint-order="stroke"
stroke-width="4"
stroke-linejoin="round"
class="fill-foreground stroke-background text-[12px] font-semibold"
>
{{ relationship.targetCardinality }}
</text>
</template>
</svg>
<div