ER / index.html
mistpe's picture
Update index.html
80bab2e verified
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ER图生成器</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 0;
overflow: hidden;
}
.container {
display: flex;
height: 100vh;
}
.editor-panel {
flex: 0 0 40%;
padding: 20px;
background-color: white;
border-right: 1px solid #ddd;
display: flex;
flex-direction: column;
overflow: auto;
}
.preview-panel {
flex: 0 0 60%;
background-color: white;
overflow: auto;
}
h1, h2, h3 {
color: #333;
margin-top: 0;
}
.title-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
textarea {
flex: 1;
padding: 10px;
font-family: monospace;
border: 1px solid #ddd;
border-radius: 4px;
resize: none;
min-height: 300px;
}
button {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
margin-top: 15px;
font-size: 16px;
}
button:hover {
background-color: #45a049;
}
.syntax-guide {
margin-top: 20px;
background-color: #f9f9f9;
padding: 15px;
border-radius: 4px;
font-size: 14px;
}
pre {
white-space: pre-wrap;
margin: 0;
font-family: monospace;
font-size: 14px;
}
input[type="text"] {
padding: 8px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
width: 300px;
}
.preview-controls {
padding: 20px;
border-bottom: 1px solid #eee;
}
#svgContainer {
padding: 20px;
}
svg {
display: block;
margin: 0 auto;
}
</style>
</head>
<body>
<div class="container">
<div class="editor-panel">
<div class="title-bar">
<h2>ER图代码编辑器</h2>
</div>
<textarea id="erCode" rows="20">// 实体定义
entity: 员工
pk: 员工编号
attr: 姓名
attr: 性别
attr: 出生日期
attr: 电话
entity: 部门
pk: 部门编号
attr: 部门名称
attr: 位置
entity: 项目
pk: 项目编号
attr: 项目名称
attr: 开始日期
attr: 结束日期
attr: 预算
entity: 客户
pk: 客户编号
attr: 客户名称
attr: 联系人
attr: 电话
relation: 隶属
from: 员工 (N)
to: 部门 (1)
relation: 管理
from: 员工 (1)
to: 项目 (N)
relation: 参与
from: 员工 (N)
to: 项目 (N)
relation: 合作
from: 项目 (N)
to: 客户 (1)</textarea>
<button id="generateBtn">生成ER图</button>
<div class="syntax-guide">
<h3>语法说明:</h3>
<pre>// 实体定义
entity: 实体名
pk: 主键字段
attr: 属性1
attr: 属性2
// 关系定义
relation: 关系名
from: 实体1 (基数)
to: 实体2 (基数)
// 基数表示: 1, N, M等</pre>
</div>
</div>
<div class="preview-panel">
<div class="preview-controls">
<div class="title-bar">
<h2>ER图预览</h2>
<div>
<span>标题: </span>
<input type="text" id="diagramTitle" value="企业人事管理系统">
</div>
</div>
</div>
<div id="svgContainer"></div>
</div>
</div>
<script>
// 解析ER图代码
function parseERCode(code) {
const entities = [];
const relations = [];
let currentEntity = null;
let currentRelation = null;
const lines = code.split('\n');
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine === '' || trimmedLine.startsWith('//')) continue;
// 实体定义
if (trimmedLine.startsWith('entity:')) {
if (currentEntity) entities.push(currentEntity);
if (currentRelation) relations.push(currentRelation);
currentEntity = {
name: trimmedLine.substring(7).trim(),
pk: null,
attributes: []
};
currentRelation = null;
}
// 主键定义
else if (trimmedLine.startsWith('pk:') && currentEntity) {
currentEntity.pk = trimmedLine.substring(3).trim();
}
// 属性定义
else if (trimmedLine.startsWith('attr:') && currentEntity) {
currentEntity.attributes.push(trimmedLine.substring(5).trim());
}
// 关系定义
else if (trimmedLine.startsWith('relation:')) {
if (currentEntity) entities.push(currentEntity);
if (currentRelation) relations.push(currentRelation);
currentEntity = null;
currentRelation = {
name: trimmedLine.substring(9).trim(),
from: null,
to: null
};
}
// 关系源
else if (trimmedLine.startsWith('from:') && currentRelation) {
const match = trimmedLine.match(/from:\s+([^\(]+)\s+\(([^\)]+)\)/);
if (match) {
currentRelation.from = {
entity: match[1].trim(),
cardinality: match[2].trim()
};
}
}
// 关系目标
else if (trimmedLine.startsWith('to:') && currentRelation) {
const match = trimmedLine.match(/to:\s+([^\(]+)\s+\(([^\)]+)\)/);
if (match) {
currentRelation.to = {
entity: match[1].trim(),
cardinality: match[2].trim()
};
}
}
}
// 添加最后一个实体或关系
if (currentEntity) entities.push(currentEntity);
if (currentRelation) relations.push(currentRelation);
return { entities, relations };
}
// 创建ER图布局
function createLayout(entities, relations) {
// 基础画布尺寸设置
const minWidth = 1200;
const minHeight = 900;
const width = Math.max(minWidth, entities.length * 300);
const height = Math.max(minHeight, entities.length * 250);
const layout = {
width: width,
height: height,
entities: {},
relations: {}
};
// 实体位置分配 - 改进的分区策略
if (entities.length <= 4) {
// 使用更合理的四角布局
const positions = [
{ x: width / 2, y: height * 0.25 }, // 上
{ x: width * 0.25, y: height / 2 }, // 左
{ x: width * 0.75, y: height / 2 }, // 右
{ x: width / 2, y: height * 0.75 } // 下
];
entities.forEach((entity, index) => {
layout.entities[entity.name] = {
x: positions[index].x,
y: positions[index].y,
width: 120,
height: 60,
attributes: [],
section: index // 记录实体所在区域(上/左/右/下)
};
});
} else {
// 多实体情况下的均匀分布
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) * 0.3;
entities.forEach((entity, index) => {
const angle = (index * 2 * Math.PI) / entities.length;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
// 确定区域象限
let section;
if (y < centerY && Math.abs(x - centerX) < radius * 0.5) section = 0; // 上
else if (x < centerX && Math.abs(y - centerY) < radius * 0.5) section = 1; // 左
else if (x > centerX && Math.abs(y - centerY) < radius * 0.5) section = 2; // 右
else section = 3; // 下
layout.entities[entity.name] = {
x: x,
y: y,
width: 120,
height: 60,
attributes: [],
section: section
};
});
}
// 属性布局优化 - 根据实体区域确定属性位置
const occupiedSpaces = []; // 记录所有已占用的空间
// 先添加所有实体占用的空间
for (const [entityName, entityLayout] of Object.entries(layout.entities)) {
occupiedSpaces.push({
x: entityLayout.x,
y: entityLayout.y,
width: entityLayout.width + 20, // 额外边距
height: entityLayout.height + 20,
type: 'entity'
});
}
// 为每个实体分配属性空间
for (const [entityName, entityLayout] of Object.entries(layout.entities)) {
const entity = entities.find(e => e.name === entityName);
if (!entity) continue;
// 收集属性
const allAttributes = [];
if (entity.pk) {
allAttributes.push({ name: entity.pk, isPk: true });
}
entity.attributes.forEach(attr => {
allAttributes.push({ name: attr, isPk: false });
});
// 根据实体区域确定属性首选方向
let preferredDirections = [];
switch(entityLayout.section) {
case 0: // 上方实体
preferredDirections = ['top', 'left', 'right', 'bottom'];
break;
case 1: // 左侧实体
preferredDirections = ['left', 'top', 'bottom', 'right'];
break;
case 2: // 右侧实体
preferredDirections = ['right', 'top', 'bottom', 'left'];
break;
case 3: // 下方实体
preferredDirections = ['bottom', 'left', 'right', 'top'];
break;
}
// 为主键找最优位置(首选方向)
let pkIndex = allAttributes.findIndex(attr => attr.isPk);
if (pkIndex >= 0) {
const pk = allAttributes[pkIndex];
const bestDirection = findBestDirection(entityLayout, pk, preferredDirections[0], occupiedSpaces);
// 放置主键
const pkPosition = positionInDirection(entityLayout, bestDirection, 120);
entityLayout.pk = {
x: pkPosition.x,
y: pkPosition.y,
width: 60,
height: 35,
name: pk.name
};
// 添加到已占用空间
occupiedSpaces.push({
x: pkPosition.x,
y: pkPosition.y,
width: 130, // 为了避免重叠增加了空间
height: 80,
type: 'pk'
});
// 从属性列表移除
allAttributes.splice(pkIndex, 1);
}
// 为普通属性找位置(尽量按首选方向,但可调整避免重叠)
allAttributes.forEach((attr, i) => {
// 尝试每个方向直到找到合适位置
let bestDirection = null;
let bestPosition = null;
// 尝试所有方向
for (const direction of preferredDirections) {
// 生成候选位置(带偏移)
for (let offset = 0; offset <= 4; offset++) {
const distance = 120 + offset * 20; // 逐渐增加距离
const angleOffset = (offset * 0.1) * (i % 2 === 0 ? 1 : -1); // 左右交替偏移
const position = positionInDirection(entityLayout, direction, distance, angleOffset);
// 检查是否与已占用空间重叠
if (!hasOverlap(position, occupiedSpaces)) {
bestDirection = direction;
bestPosition = position;
break;
}
}
if (bestPosition) break; // 找到了就停止寻找
}
// 如果没找到无重叠位置,取最后一个方向的位置并强制增加距离
if (!bestPosition) {
const lastDirection = preferredDirections[preferredDirections.length - 1];
bestPosition = positionInDirection(entityLayout, lastDirection, 200 + i * 30);
}
// 放置属性
const attrObj = {
x: bestPosition.x,
y: bestPosition.y,
width: 55,
height: 30,
name: attr.name
};
entityLayout.attributes.push(attrObj);
// 添加到已占用空间
occupiedSpaces.push({
x: bestPosition.x,
y: bestPosition.y,
width: 120,
height: 70,
type: 'attr'
});
});
}
// 关系位置优化 - 动态调整关系菱形位置
relations.forEach(relation => {
const fromEntity = layout.entities[relation.from.entity];
const toEntity = layout.entities[relation.to.entity];
if (fromEntity && toEntity) {
// 计算两实体中点
const dx = toEntity.x - fromEntity.x;
const dy = toEntity.y - fromEntity.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 动态调整居中比例 - 距离越远比例越接近0.5
const ratio = 0.4 + (distance / 1500); // 调整系数使布局更紧凑
const midX = fromEntity.x + dx * ratio;
const midY = fromEntity.y + dy * ratio;
// 寻找无重叠位置
let bestX = midX;
let bestY = midY;
let minOverlaps = Number.MAX_VALUE;
// 在中点附近探索多个位置
for (let offsetX = -60; offsetX <= 60; offsetX += 20) {
for (let offsetY = -60; offsetY <= 60; offsetY += 20) {
const testX = midX + offsetX;
const testY = midY + offsetY;
// 计算此位置与已有组件的重叠数
let overlaps = 0;
for (const space of occupiedSpaces) {
if (isPointInRect(testX, testY, space)) {
overlaps++;
}
}
// 如果有更少重叠,更新最佳位置
if (overlaps < minOverlaps) {
minOverlaps = overlaps;
bestX = testX;
bestY = testY;
// 如果没有重叠,立即使用
if (overlaps === 0) break;
}
}
if (minOverlaps === 0) break;
}
// 如果还是有重叠,再增加搜索范围
if (minOverlaps > 0) {
const perpX = -dy / distance * 100; // 垂直于连线方向
const perpY = dx / distance * 100;
// 尝试垂直偏移
bestX = midX + perpX;
bestY = midY + perpY;
// 检查是否仍有重叠
let stillOverlap = false;
for (const space of occupiedSpaces) {
if (isPointInRect(bestX, bestY, space)) {
stillOverlap = true;
break;
}
}
// 如有必要,尝试反方向
if (stillOverlap) {
bestX = midX - perpX;
bestY = midY - perpY;
}
}
// 保存最终关系位置
layout.relations[relation.name] = {
x: bestX,
y: bestY,
width: 45,
height: 25,
from: relation.from,
to: relation.to
};
// 添加到已占用空间
occupiedSpaces.push({
x: bestX,
y: bestY,
width: 100,
height: 60,
type: 'relation'
});
}
});
return layout;
}
// 辅助函数: 检查位置是否与已占用空间重叠
function hasOverlap(position, occupiedSpaces) {
const testRect = {
x: position.x,
y: position.y,
width: 120, // 属性空间
height: 70
};
for (const space of occupiedSpaces) {
if (doRectsOverlap(testRect, space)) {
return true;
}
}
return false;
}
// 矩形重叠检测
function doRectsOverlap(rect1, rect2) {
const minDistance = (rect1.width + rect2.width) / 2 * 0.8; // 80%宽度作为水平安全距离
const minVertical = (rect1.height + rect2.height) / 2 * 0.8; // 80%高度作为垂直安全距离
const dx = Math.abs(rect1.x - rect2.x);
const dy = Math.abs(rect1.y - rect2.y);
return dx < minDistance && dy < minVertical;
}
// 检查点是否在矩形内(或距离过近)
function isPointInRect(x, y, rect) {
const halfWidth = rect.width / 2;
const halfHeight = rect.height / 2;
return Math.abs(x - rect.x) < halfWidth &&
Math.abs(y - rect.y) < halfHeight;
}
// 在指定方向上找位置(带角度偏移)
function positionInDirection(entityLayout, direction, distance, angleOffset = 0) {
let angle;
switch(direction) {
case 'top':
angle = -Math.PI/2 + (angleOffset || 0);
break;
case 'right':
angle = 0 + (angleOffset || 0);
break;
case 'bottom':
angle = Math.PI/2 + (angleOffset || 0);
break;
case 'left':
angle = Math.PI + (angleOffset || 0);
break;
}
return {
x: entityLayout.x + Math.cos(angle) * distance,
y: entityLayout.y + Math.sin(angle) * distance
};
}
// 找到最佳方向(最少重叠)
function findBestDirection(entityLayout, attr, preferredDirection, occupiedSpaces) {
const directions = ['top', 'right', 'bottom', 'left'];
let bestDirection = preferredDirection;
let minOverlaps = Number.MAX_VALUE;
// 尝试所有方向
for (const direction of directions) {
const position = positionInDirection(entityLayout, direction, 120);
// 计算此方向的重叠
let overlaps = 0;
for (const space of occupiedSpaces) {
if (isPointInRect(position.x, position.y, space)) {
overlaps++;
}
}
// 优先考虑首选方向,但也要权衡重叠情况
const directionPenalty = (direction === preferredDirection) ? 0 : 1;
const score = overlaps + directionPenalty;
if (score < minOverlaps) {
minOverlaps = score;
bestDirection = direction;
}
}
return bestDirection;
}
// 计算实体与关系之间的连接点
function calculateConnectionPoint(entity, relation) {
const dx = relation.x - entity.x;
const dy = relation.y - entity.y;
const angle = Math.atan2(dy, dx);
// 计算矩形边界上的交点
let intersectX, intersectY;
// 计算水平和垂直方向的交点距离
const width = entity.width / 2;
const height = entity.height / 2;
// 处理特殊情况 - 垂直或水平线
if (Math.abs(dx) < 0.001) { // 垂直线
intersectX = entity.x;
intersectY = entity.y + Math.sign(dy) * height;
} else if (Math.abs(dy) < 0.001) { // 水平线
intersectX = entity.x + Math.sign(dx) * width;
intersectY = entity.y;
} else {
// 一般情况 - 计算与矩形边界的交点
const slope = dy / dx;
const invSlope = dx / dy;
// 检查与垂直边界的交点
const vertX = entity.x + Math.sign(dx) * width;
const vertY = entity.y + slope * (vertX - entity.x);
// 检查与水平边界的交点
const horizY = entity.y + Math.sign(dy) * height;
const horizX = entity.x + invSlope * (horizY - entity.y);
// 选择最接近实体中心的交点
const dxVert = Math.abs(vertX - entity.x);
const dyVert = Math.abs(vertY - entity.y);
const dVert = Math.sqrt(dxVert * dxVert + dyVert * dyVert);
const dxHoriz = Math.abs(horizX - entity.x);
const dyHoriz = Math.abs(horizY - entity.y);
const dHoriz = Math.sqrt(dxHoriz * dxHoriz + dyHoriz * dyHoriz);
if (dVert <= dHoriz && Math.abs(vertY - entity.y) <= height) {
intersectX = vertX;
intersectY = vertY;
} else if (Math.abs(horizX - entity.x) <= width) {
intersectX = horizX;
intersectY = horizY;
} else {
// 边缘情况 - 使用角点
intersectX = entity.x + Math.sign(dx) * width;
intersectY = entity.y + Math.sign(dy) * height;
}
}
return { x: intersectX, y: intersectY };
}
// 计算实体到属性的连接点
function calculateEntityToAttributeConnection(entity, attribute) {
const dx = attribute.x - entity.x;
const dy = attribute.y - entity.y;
const angle = Math.atan2(dy, dx);
// 同样的方法,但简化版本
const width = entity.width / 2;
const height = entity.height / 2;
// 考虑特殊情况
if (Math.abs(dx) < 0.001) { // 垂直线
return {
x: entity.x,
y: entity.y + Math.sign(dy) * height
};
} else if (Math.abs(dy) < 0.001) { // 水平线
return {
x: entity.x + Math.sign(dx) * width,
y: entity.y
};
} else {
// 一般情况
const slope = dy / dx;
// 比较水平和垂直交点
const tx = width / Math.abs(Math.cos(angle));
const ty = height / Math.abs(Math.sin(angle));
if (tx < ty) {
return {
x: entity.x + Math.sign(dx) * width,
y: entity.y + slope * Math.sign(dx) * width
};
} else {
return {
x: entity.x + Math.sign(dy) * height / slope,
y: entity.y + Math.sign(dy) * height
};
}
}
}
// 计算属性到实体的连接点 (用于椭圆)
function calculateAttributeToEntityConnection(attribute, entity) {
const dx = entity.x - attribute.x;
const dy = entity.y - attribute.y;
const angle = Math.atan2(dy, dx);
// 椭圆上的点
return {
x: attribute.x + Math.cos(angle) * attribute.width,
y: attribute.y + Math.sin(angle) * attribute.height
};
}
// 生成SVG
function generateSVG(entities, relations, layout, title) {
// 首先准备SVG容器
let svgContent = `
<svg width="${layout.width}" height="${layout.height}" xmlns="http://www.w3.org/2000/svg">
`;
// 添加标题
svgContent += `
<text x="${layout.width/2}" y="40" text-anchor="middle" font-size="24" font-weight="bold" fill="#333">${title}</text>
`;
// 创建分层SVG - 确保正确的Z顺序
// 1. 底层:所有连接线
let linesLayer = '<g id="lines-layer">\n';
// 2. 实体-关系连接线
for (const [relationName, relationLayout] of Object.entries(layout.relations)) {
const fromEntity = layout.entities[relationLayout.from.entity];
const toEntity = layout.entities[relationLayout.to.entity];
if (fromEntity && toEntity) {
// 计算精确的连接点
const fromConnection = calculateConnectionPoint(fromEntity, relationLayout);
const toConnection = calculateConnectionPoint(toEntity, relationLayout);
// 实体-关系连接线
linesLayer += `
<line x1="${fromConnection.x}" y1="${fromConnection.y}" x2="${relationLayout.x}" y2="${relationLayout.y}"
stroke="#6c757d" stroke-width="2" />
<line x1="${relationLayout.x}" y1="${relationLayout.y}" x2="${toConnection.x}" y2="${toConnection.y}"
stroke="#6c757d" stroke-width="2" />
`;
// 基数标记位置 - 距离实体连接点1/4处
const fromCardX = fromConnection.x + (relationLayout.x - fromConnection.x) * 0.25;
const fromCardY = fromConnection.y + (relationLayout.y - fromConnection.y) * 0.25;
const toCardX = toConnection.x + (relationLayout.x - toConnection.x) * 0.25;
const toCardY = toConnection.y + (relationLayout.y - toConnection.y) * 0.25;
// 基数文本
linesLayer += `
<text x="${fromCardX}" y="${fromCardY}" text-anchor="middle" dominant-baseline="middle"
font-weight="bold" font-size="16" fill="#dc3545">${relationLayout.from.cardinality}</text>
<text x="${toCardX}" y="${toCardY}" text-anchor="middle" dominant-baseline="middle"
font-weight="bold" font-size="16" fill="#dc3545">${relationLayout.to.cardinality}</text>
`;
}
}
// 实体-属性连接线
for (const [entityName, entityLayout] of Object.entries(layout.entities)) {
// 主键连接线
if (entityLayout.pk) {
const pk = entityLayout.pk;
// 计算精确的连接点
const entityConnection = calculateEntityToAttributeConnection(entityLayout, pk);
const pkConnection = calculateAttributeToEntityConnection(pk, entityLayout);
linesLayer += `
<line x1="${entityConnection.x}" y1="${entityConnection.y}" x2="${pkConnection.x}" y2="${pkConnection.y}"
stroke="#dc3545" stroke-width="1.5" />
`;
}
// 普通属性连接线
entityLayout.attributes.forEach(attr => {
// 计算精确的连接点
const entityConnection = calculateEntityToAttributeConnection(entityLayout, attr);
const attrConnection = calculateAttributeToEntityConnection(attr, entityLayout);
linesLayer += `
<line x1="${entityConnection.x}" y1="${entityConnection.y}" x2="${attrConnection.x}" y2="${attrConnection.y}"
stroke="#28a745" stroke-width="1.5" />
`;
});
}
linesLayer += '</g>\n';
// 3. 中层:关系菱形
let relationshipsLayer = '<g id="relationships-layer">\n';
for (const [relationName, relationLayout] of Object.entries(layout.relations)) {
const x = relationLayout.x;
const y = relationLayout.y;
const w = relationLayout.width;
const h = relationLayout.height;
relationshipsLayer += `
<polygon points="${x},${y-h} ${x+w},${y} ${x},${y+h} ${x-w},${y}"
fill="#fff3cd" stroke="#ffc107" stroke-width="1.5" />
<text x="${x}" y="${y}" text-anchor="middle" dominant-baseline="middle"
font-size="14" fill="#212529">${relationName}</text>
`;
}
relationshipsLayer += '</g>\n';
// 4. 上层:实体矩形和属性椭圆
let entitiesLayer = '<g id="entities-layer">\n';
for (const [entityName, entityLayout] of Object.entries(layout.entities)) {
// 绘制属性椭圆
// 先画主键
if (entityLayout.pk) {
const pk = entityLayout.pk;
entitiesLayer += `
<ellipse cx="${pk.x}" cy="${pk.y}" rx="${pk.width}" ry="${pk.height}"
fill="#ffebee" stroke="#dc3545" stroke-width="2" />
<text x="${pk.x}" y="${pk.y}" text-anchor="middle" dominant-baseline="middle"
font-size="14" font-weight="bold">${pk.name}</text>
`;
}
// 普通属性
entityLayout.attributes.forEach(attr => {
entitiesLayer += `
<ellipse cx="${attr.x}" cy="${attr.y}" rx="${attr.width}" ry="${attr.height}"
fill="#f8f9fa" stroke="#28a745" stroke-width="1.5" />
<text x="${attr.x}" y="${attr.y}" text-anchor="middle" dominant-baseline="middle"
font-size="14">${attr.name}</text>
`;
});
// 绘制实体矩形
entitiesLayer += `
<rect x="${entityLayout.x - entityLayout.width/2}" y="${entityLayout.y - entityLayout.height/2}"
width="${entityLayout.width}" height="${entityLayout.height}" rx="5" ry="5"
fill="#e3f2fd" stroke="#0d6efd" stroke-width="2" />
<text x="${entityLayout.x}" y="${entityLayout.y}" text-anchor="middle" dominant-baseline="middle"
font-size="16" font-weight="bold" fill="#0d6efd">${entityName}</text>
`;
}
entitiesLayer += '</g>\n';
// 组合所有层级,确保正确的Z轴顺序
svgContent += linesLayer; // 最底层:连接线
svgContent += relationshipsLayer; // 中间层:关系菱形
svgContent += entitiesLayer; // 最上层:实体和属性
svgContent += '</svg>';
return svgContent;
}
// 生成ER图
function generateERDiagram() {
const erCode = document.getElementById('erCode').value;
const title = document.getElementById('diagramTitle').value || 'ER图';
try {
const { entities, relations } = parseERCode(erCode);
const layout = createLayout(entities, relations);
const svgContent = generateSVG(entities, relations, layout, title);
document.getElementById('svgContainer').innerHTML = svgContent;
} catch (error) {
console.error('生成ER图时出错:', error);
alert('生成ER图时出错: ' + error.message);
}
}
// 初始加载和事件监听
document.addEventListener('DOMContentLoaded', function() {
// 初始生成
generateERDiagram();
// 绑定生成按钮事件
document.getElementById('generateBtn').addEventListener('click', generateERDiagram);
// 绑定标题更改事件
document.getElementById('diagramTitle').addEventListener('change', generateERDiagram);
});
</script>
</body>
</html>