mistpe commited on
Commit
80bab2e
·
verified ·
1 Parent(s): 2e9eb5e

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +932 -19
index.html CHANGED
@@ -1,19 +1,932 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ER图生成器</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ background-color: #f5f5f5;
11
+ margin: 0;
12
+ padding: 0;
13
+ overflow: hidden;
14
+ }
15
+ .container {
16
+ display: flex;
17
+ height: 100vh;
18
+ }
19
+ .editor-panel {
20
+ flex: 0 0 40%;
21
+ padding: 20px;
22
+ background-color: white;
23
+ border-right: 1px solid #ddd;
24
+ display: flex;
25
+ flex-direction: column;
26
+ overflow: auto;
27
+ }
28
+ .preview-panel {
29
+ flex: 0 0 60%;
30
+ background-color: white;
31
+ overflow: auto;
32
+ }
33
+ h1, h2, h3 {
34
+ color: #333;
35
+ margin-top: 0;
36
+ }
37
+ .title-bar {
38
+ display: flex;
39
+ justify-content: space-between;
40
+ align-items: center;
41
+ margin-bottom: 15px;
42
+ padding-bottom: 15px;
43
+ border-bottom: 1px solid #eee;
44
+ }
45
+ textarea {
46
+ flex: 1;
47
+ padding: 10px;
48
+ font-family: monospace;
49
+ border: 1px solid #ddd;
50
+ border-radius: 4px;
51
+ resize: none;
52
+ min-height: 300px;
53
+ }
54
+ button {
55
+ background-color: #4CAF50;
56
+ color: white;
57
+ border: none;
58
+ padding: 10px 15px;
59
+ border-radius: 4px;
60
+ cursor: pointer;
61
+ margin-top: 15px;
62
+ font-size: 16px;
63
+ }
64
+ button:hover {
65
+ background-color: #45a049;
66
+ }
67
+ .syntax-guide {
68
+ margin-top: 20px;
69
+ background-color: #f9f9f9;
70
+ padding: 15px;
71
+ border-radius: 4px;
72
+ font-size: 14px;
73
+ }
74
+ pre {
75
+ white-space: pre-wrap;
76
+ margin: 0;
77
+ font-family: monospace;
78
+ font-size: 14px;
79
+ }
80
+ input[type="text"] {
81
+ padding: 8px 10px;
82
+ border: 1px solid #ddd;
83
+ border-radius: 4px;
84
+ font-size: 16px;
85
+ width: 300px;
86
+ }
87
+ .preview-controls {
88
+ padding: 20px;
89
+ border-bottom: 1px solid #eee;
90
+ }
91
+ #svgContainer {
92
+ padding: 20px;
93
+ }
94
+ svg {
95
+ display: block;
96
+ margin: 0 auto;
97
+ }
98
+ </style>
99
+ </head>
100
+ <body>
101
+ <div class="container">
102
+ <div class="editor-panel">
103
+ <div class="title-bar">
104
+ <h2>ER图代码编辑器</h2>
105
+ </div>
106
+ <textarea id="erCode" rows="20">// 实体定义
107
+ entity: 员工
108
+ pk: 员工编号
109
+ attr: 姓名
110
+ attr: 性别
111
+ attr: 出生日期
112
+ attr: 电话
113
+
114
+ entity: 部门
115
+ pk: 部门编号
116
+ attr: 部门名称
117
+ attr: 位置
118
+
119
+ entity: 项目
120
+ pk: 项目编号
121
+ attr: 项目名称
122
+ attr: 开始日期
123
+ attr: 结束日期
124
+ attr: 预算
125
+
126
+ entity: 客户
127
+ pk: 客户编号
128
+ attr: 客户名称
129
+ attr: 联系人
130
+ attr: 电话
131
+
132
+ relation: 隶属
133
+ from: 员工 (N)
134
+ to: 部门 (1)
135
+
136
+ relation: 管理
137
+ from: 员工 (1)
138
+ to: 项目 (N)
139
+
140
+ relation: 参与
141
+ from: 员工 (N)
142
+ to: 项目 (N)
143
+
144
+ relation: 合作
145
+ from: 项目 (N)
146
+ to: 客户 (1)</textarea>
147
+ <button id="generateBtn">生成ER图</button>
148
+ <div class="syntax-guide">
149
+ <h3>语法说明:</h3>
150
+ <pre>// 实体定义
151
+ entity: 实体名
152
+ pk: 主键字段
153
+ attr: 属性1
154
+ attr: 属性2
155
+
156
+ // 关系定义
157
+ relation: 关系名
158
+ from: 实体1 (基数)
159
+ to: 实体2 (基数)
160
+
161
+ // 基数表示: 1, N, M等</pre>
162
+ </div>
163
+ </div>
164
+ <div class="preview-panel">
165
+ <div class="preview-controls">
166
+ <div class="title-bar">
167
+ <h2>ER图预览</h2>
168
+ <div>
169
+ <span>标题: </span>
170
+ <input type="text" id="diagramTitle" value="企业人事管理系统">
171
+ </div>
172
+ </div>
173
+ </div>
174
+ <div id="svgContainer"></div>
175
+ </div>
176
+ </div>
177
+
178
+ <script>
179
+ // 解析ER图代码
180
+ function parseERCode(code) {
181
+ const entities = [];
182
+ const relations = [];
183
+
184
+ let currentEntity = null;
185
+ let currentRelation = null;
186
+
187
+ const lines = code.split('\n');
188
+
189
+ for (const line of lines) {
190
+ const trimmedLine = line.trim();
191
+ if (trimmedLine === '' || trimmedLine.startsWith('//')) continue;
192
+
193
+ // 实体定义
194
+ if (trimmedLine.startsWith('entity:')) {
195
+ if (currentEntity) entities.push(currentEntity);
196
+ if (currentRelation) relations.push(currentRelation);
197
+
198
+ currentEntity = {
199
+ name: trimmedLine.substring(7).trim(),
200
+ pk: null,
201
+ attributes: []
202
+ };
203
+ currentRelation = null;
204
+ }
205
+ // 主键定义
206
+ else if (trimmedLine.startsWith('pk:') && currentEntity) {
207
+ currentEntity.pk = trimmedLine.substring(3).trim();
208
+ }
209
+ // 属性定义
210
+ else if (trimmedLine.startsWith('attr:') && currentEntity) {
211
+ currentEntity.attributes.push(trimmedLine.substring(5).trim());
212
+ }
213
+ // 关系定义
214
+ else if (trimmedLine.startsWith('relation:')) {
215
+ if (currentEntity) entities.push(currentEntity);
216
+ if (currentRelation) relations.push(currentRelation);
217
+
218
+ currentEntity = null;
219
+ currentRelation = {
220
+ name: trimmedLine.substring(9).trim(),
221
+ from: null,
222
+ to: null
223
+ };
224
+ }
225
+ // 关系源
226
+ else if (trimmedLine.startsWith('from:') && currentRelation) {
227
+ const match = trimmedLine.match(/from:\s+([^\(]+)\s+\(([^\)]+)\)/);
228
+ if (match) {
229
+ currentRelation.from = {
230
+ entity: match[1].trim(),
231
+ cardinality: match[2].trim()
232
+ };
233
+ }
234
+ }
235
+ // 关系目标
236
+ else if (trimmedLine.startsWith('to:') && currentRelation) {
237
+ const match = trimmedLine.match(/to:\s+([^\(]+)\s+\(([^\)]+)\)/);
238
+ if (match) {
239
+ currentRelation.to = {
240
+ entity: match[1].trim(),
241
+ cardinality: match[2].trim()
242
+ };
243
+ }
244
+ }
245
+ }
246
+
247
+ // 添加最后一个实体或关系
248
+ if (currentEntity) entities.push(currentEntity);
249
+ if (currentRelation) relations.push(currentRelation);
250
+
251
+ return { entities, relations };
252
+ }
253
+
254
+ // 创建ER图布局
255
+ function createLayout(entities, relations) {
256
+ // 基础画布尺寸设置
257
+ const minWidth = 1200;
258
+ const minHeight = 900;
259
+ const width = Math.max(minWidth, entities.length * 300);
260
+ const height = Math.max(minHeight, entities.length * 250);
261
+
262
+ const layout = {
263
+ width: width,
264
+ height: height,
265
+ entities: {},
266
+ relations: {}
267
+ };
268
+
269
+ // 实体位置分配 - 改进的分区策略
270
+ if (entities.length <= 4) {
271
+ // 使用更合理的四角布局
272
+ const positions = [
273
+ { x: width / 2, y: height * 0.25 }, // 上
274
+ { x: width * 0.25, y: height / 2 }, // 左
275
+ { x: width * 0.75, y: height / 2 }, // 右
276
+ { x: width / 2, y: height * 0.75 } // 下
277
+ ];
278
+
279
+ entities.forEach((entity, index) => {
280
+ layout.entities[entity.name] = {
281
+ x: positions[index].x,
282
+ y: positions[index].y,
283
+ width: 120,
284
+ height: 60,
285
+ attributes: [],
286
+ section: index // 记录实体所在区域(上/左/右/下)
287
+ };
288
+ });
289
+ } else {
290
+ // 多实体情况下的均匀分布
291
+ const centerX = width / 2;
292
+ const centerY = height / 2;
293
+ const radius = Math.min(width, height) * 0.3;
294
+
295
+ entities.forEach((entity, index) => {
296
+ const angle = (index * 2 * Math.PI) / entities.length;
297
+ const x = centerX + radius * Math.cos(angle);
298
+ const y = centerY + radius * Math.sin(angle);
299
+
300
+ // 确定���域象限
301
+ let section;
302
+ if (y < centerY && Math.abs(x - centerX) < radius * 0.5) section = 0; // 上
303
+ else if (x < centerX && Math.abs(y - centerY) < radius * 0.5) section = 1; // 左
304
+ else if (x > centerX && Math.abs(y - centerY) < radius * 0.5) section = 2; // 右
305
+ else section = 3; // 下
306
+
307
+ layout.entities[entity.name] = {
308
+ x: x,
309
+ y: y,
310
+ width: 120,
311
+ height: 60,
312
+ attributes: [],
313
+ section: section
314
+ };
315
+ });
316
+ }
317
+
318
+ // 属性布局优化 - 根据实体区域确定属性位置
319
+ const occupiedSpaces = []; // 记录所有已占用的空间
320
+
321
+ // 先添加所有实体占用的空间
322
+ for (const [entityName, entityLayout] of Object.entries(layout.entities)) {
323
+ occupiedSpaces.push({
324
+ x: entityLayout.x,
325
+ y: entityLayout.y,
326
+ width: entityLayout.width + 20, // 额外边距
327
+ height: entityLayout.height + 20,
328
+ type: 'entity'
329
+ });
330
+ }
331
+
332
+ // 为每个实体分配属性空间
333
+ for (const [entityName, entityLayout] of Object.entries(layout.entities)) {
334
+ const entity = entities.find(e => e.name === entityName);
335
+ if (!entity) continue;
336
+
337
+ // 收集属性
338
+ const allAttributes = [];
339
+ if (entity.pk) {
340
+ allAttributes.push({ name: entity.pk, isPk: true });
341
+ }
342
+
343
+ entity.attributes.forEach(attr => {
344
+ allAttributes.push({ name: attr, isPk: false });
345
+ });
346
+
347
+ // 根据实体区域确定属性首选方向
348
+ let preferredDirections = [];
349
+
350
+ switch(entityLayout.section) {
351
+ case 0: // 上方实体
352
+ preferredDirections = ['top', 'left', 'right', 'bottom'];
353
+ break;
354
+ case 1: // 左侧实体
355
+ preferredDirections = ['left', 'top', 'bottom', 'right'];
356
+ break;
357
+ case 2: // 右侧实体
358
+ preferredDirections = ['right', 'top', 'bottom', 'left'];
359
+ break;
360
+ case 3: // 下方实体
361
+ preferredDirections = ['bottom', 'left', 'right', 'top'];
362
+ break;
363
+ }
364
+
365
+ // 为主键找最优位置(首选方向)
366
+ let pkIndex = allAttributes.findIndex(attr => attr.isPk);
367
+ if (pkIndex >= 0) {
368
+ const pk = allAttributes[pkIndex];
369
+ const bestDirection = findBestDirection(entityLayout, pk, preferredDirections[0], occupiedSpaces);
370
+
371
+ // 放置主键
372
+ const pkPosition = positionInDirection(entityLayout, bestDirection, 120);
373
+
374
+ entityLayout.pk = {
375
+ x: pkPosition.x,
376
+ y: pkPosition.y,
377
+ width: 60,
378
+ height: 35,
379
+ name: pk.name
380
+ };
381
+
382
+ // 添加到已占用空间
383
+ occupiedSpaces.push({
384
+ x: pkPosition.x,
385
+ y: pkPosition.y,
386
+ width: 130, // 为了避免重叠增加了空间
387
+ height: 80,
388
+ type: 'pk'
389
+ });
390
+
391
+ // 从属性列表移除
392
+ allAttributes.splice(pkIndex, 1);
393
+ }
394
+
395
+ // 为普通属性找位置(尽量按首选方向,但可调整避免重叠)
396
+ allAttributes.forEach((attr, i) => {
397
+ // 尝试每个方向直到找到合适位置
398
+ let bestDirection = null;
399
+ let bestPosition = null;
400
+
401
+ // 尝试所有方向
402
+ for (const direction of preferredDirections) {
403
+ // 生成候选位置(带偏移)
404
+ for (let offset = 0; offset <= 4; offset++) {
405
+ const distance = 120 + offset * 20; // 逐渐增加距离
406
+ const angleOffset = (offset * 0.1) * (i % 2 === 0 ? 1 : -1); // 左右交替偏移
407
+
408
+ const position = positionInDirection(entityLayout, direction, distance, angleOffset);
409
+
410
+ // 检查是否与已占用空间重叠
411
+ if (!hasOverlap(position, occupiedSpaces)) {
412
+ bestDirection = direction;
413
+ bestPosition = position;
414
+ break;
415
+ }
416
+ }
417
+
418
+ if (bestPosition) break; // 找到了就停止寻找
419
+ }
420
+
421
+ // 如果没找到无重叠位置,取最后一个方向的位置并强制增加距离
422
+ if (!bestPosition) {
423
+ const lastDirection = preferredDirections[preferredDirections.length - 1];
424
+ bestPosition = positionInDirection(entityLayout, lastDirection, 200 + i * 30);
425
+ }
426
+
427
+ // 放置属性
428
+ const attrObj = {
429
+ x: bestPosition.x,
430
+ y: bestPosition.y,
431
+ width: 55,
432
+ height: 30,
433
+ name: attr.name
434
+ };
435
+
436
+ entityLayout.attributes.push(attrObj);
437
+
438
+ // 添加到已占用空间
439
+ occupiedSpaces.push({
440
+ x: bestPosition.x,
441
+ y: bestPosition.y,
442
+ width: 120,
443
+ height: 70,
444
+ type: 'attr'
445
+ });
446
+ });
447
+ }
448
+
449
+ // 关系位置优化 - 动态调整关系菱形位置
450
+ relations.forEach(relation => {
451
+ const fromEntity = layout.entities[relation.from.entity];
452
+ const toEntity = layout.entities[relation.to.entity];
453
+
454
+ if (fromEntity && toEntity) {
455
+ // 计算两实体中点
456
+ const dx = toEntity.x - fromEntity.x;
457
+ const dy = toEntity.y - fromEntity.y;
458
+ const distance = Math.sqrt(dx * dx + dy * dy);
459
+
460
+ // 动态调整居中比例 - 距离越远比例越接近0.5
461
+ const ratio = 0.4 + (distance / 1500); // 调整系数使布局更紧凑
462
+ const midX = fromEntity.x + dx * ratio;
463
+ const midY = fromEntity.y + dy * ratio;
464
+
465
+ // 寻找无重叠位置
466
+ let bestX = midX;
467
+ let bestY = midY;
468
+ let minOverlaps = Number.MAX_VALUE;
469
+
470
+ // 在中点附近探索多个位置
471
+ for (let offsetX = -60; offsetX <= 60; offsetX += 20) {
472
+ for (let offsetY = -60; offsetY <= 60; offsetY += 20) {
473
+ const testX = midX + offsetX;
474
+ const testY = midY + offsetY;
475
+
476
+ // 计算此位置与已有组件的重叠数
477
+ let overlaps = 0;
478
+ for (const space of occupiedSpaces) {
479
+ if (isPointInRect(testX, testY, space)) {
480
+ overlaps++;
481
+ }
482
+ }
483
+
484
+ // 如果有更少重叠,更新最佳位置
485
+ if (overlaps < minOverlaps) {
486
+ minOverlaps = overlaps;
487
+ bestX = testX;
488
+ bestY = testY;
489
+
490
+ // 如果没有重叠,立即使用
491
+ if (overlaps === 0) break;
492
+ }
493
+ }
494
+ if (minOverlaps === 0) break;
495
+ }
496
+
497
+ // 如果还是有重叠,再增加搜索范围
498
+ if (minOverlaps > 0) {
499
+ const perpX = -dy / distance * 100; // 垂直于连线方向
500
+ const perpY = dx / distance * 100;
501
+
502
+ // 尝试垂直偏移
503
+ bestX = midX + perpX;
504
+ bestY = midY + perpY;
505
+
506
+ // 检查是否仍有重叠
507
+ let stillOverlap = false;
508
+ for (const space of occupiedSpaces) {
509
+ if (isPointInRect(bestX, bestY, space)) {
510
+ stillOverlap = true;
511
+ break;
512
+ }
513
+ }
514
+
515
+ // ��有必要,尝试反方向
516
+ if (stillOverlap) {
517
+ bestX = midX - perpX;
518
+ bestY = midY - perpY;
519
+ }
520
+ }
521
+
522
+ // 保存最终关系位置
523
+ layout.relations[relation.name] = {
524
+ x: bestX,
525
+ y: bestY,
526
+ width: 45,
527
+ height: 25,
528
+ from: relation.from,
529
+ to: relation.to
530
+ };
531
+
532
+ // 添加到已占用空间
533
+ occupiedSpaces.push({
534
+ x: bestX,
535
+ y: bestY,
536
+ width: 100,
537
+ height: 60,
538
+ type: 'relation'
539
+ });
540
+ }
541
+ });
542
+
543
+ return layout;
544
+ }
545
+
546
+ // 辅助函数: 检查位置是否与已占用空间重叠
547
+ function hasOverlap(position, occupiedSpaces) {
548
+ const testRect = {
549
+ x: position.x,
550
+ y: position.y,
551
+ width: 120, // 属性空间
552
+ height: 70
553
+ };
554
+
555
+ for (const space of occupiedSpaces) {
556
+ if (doRectsOverlap(testRect, space)) {
557
+ return true;
558
+ }
559
+ }
560
+
561
+ return false;
562
+ }
563
+
564
+ // 矩形重叠检测
565
+ function doRectsOverlap(rect1, rect2) {
566
+ const minDistance = (rect1.width + rect2.width) / 2 * 0.8; // 80%宽度作为水平安全距离
567
+ const minVertical = (rect1.height + rect2.height) / 2 * 0.8; // 80%高度作为垂直安全距离
568
+
569
+ const dx = Math.abs(rect1.x - rect2.x);
570
+ const dy = Math.abs(rect1.y - rect2.y);
571
+
572
+ return dx < minDistance && dy < minVertical;
573
+ }
574
+
575
+ // 检查点是否在矩形内(或距离过近)
576
+ function isPointInRect(x, y, rect) {
577
+ const halfWidth = rect.width / 2;
578
+ const halfHeight = rect.height / 2;
579
+
580
+ return Math.abs(x - rect.x) < halfWidth &&
581
+ Math.abs(y - rect.y) < halfHeight;
582
+ }
583
+
584
+ // 在指定方向上找位置(带角度偏移)
585
+ function positionInDirection(entityLayout, direction, distance, angleOffset = 0) {
586
+ let angle;
587
+
588
+ switch(direction) {
589
+ case 'top':
590
+ angle = -Math.PI/2 + (angleOffset || 0);
591
+ break;
592
+ case 'right':
593
+ angle = 0 + (angleOffset || 0);
594
+ break;
595
+ case 'bottom':
596
+ angle = Math.PI/2 + (angleOffset || 0);
597
+ break;
598
+ case 'left':
599
+ angle = Math.PI + (angleOffset || 0);
600
+ break;
601
+ }
602
+
603
+ return {
604
+ x: entityLayout.x + Math.cos(angle) * distance,
605
+ y: entityLayout.y + Math.sin(angle) * distance
606
+ };
607
+ }
608
+
609
+ // 找到最佳方向(最少重叠)
610
+ function findBestDirection(entityLayout, attr, preferredDirection, occupiedSpaces) {
611
+ const directions = ['top', 'right', 'bottom', 'left'];
612
+ let bestDirection = preferredDirection;
613
+ let minOverlaps = Number.MAX_VALUE;
614
+
615
+ // 尝试所有方向
616
+ for (const direction of directions) {
617
+ const position = positionInDirection(entityLayout, direction, 120);
618
+
619
+ // 计算此方向的重叠
620
+ let overlaps = 0;
621
+ for (const space of occupiedSpaces) {
622
+ if (isPointInRect(position.x, position.y, space)) {
623
+ overlaps++;
624
+ }
625
+ }
626
+
627
+ // 优先考虑首选方向,但也要权衡重叠情况
628
+ const directionPenalty = (direction === preferredDirection) ? 0 : 1;
629
+ const score = overlaps + directionPenalty;
630
+
631
+ if (score < minOverlaps) {
632
+ minOverlaps = score;
633
+ bestDirection = direction;
634
+ }
635
+ }
636
+
637
+ return bestDirection;
638
+ }
639
+
640
+ // 计算实体与关系之间的连接点
641
+ function calculateConnectionPoint(entity, relation) {
642
+ const dx = relation.x - entity.x;
643
+ const dy = relation.y - entity.y;
644
+ const angle = Math.atan2(dy, dx);
645
+
646
+ // 计算矩形边界上的交点
647
+ let intersectX, intersectY;
648
+
649
+ // 计算水平和垂直方向的交点距离
650
+ const width = entity.width / 2;
651
+ const height = entity.height / 2;
652
+
653
+ // 处理特殊情况 - 垂直或水平线
654
+ if (Math.abs(dx) < 0.001) { // 垂直线
655
+ intersectX = entity.x;
656
+ intersectY = entity.y + Math.sign(dy) * height;
657
+ } else if (Math.abs(dy) < 0.001) { // 水平线
658
+ intersectX = entity.x + Math.sign(dx) * width;
659
+ intersectY = entity.y;
660
+ } else {
661
+ // 一般情况 - 计算与矩形边界的交点
662
+ const slope = dy / dx;
663
+ const invSlope = dx / dy;
664
+
665
+ // 检查与垂直边界的交点
666
+ const vertX = entity.x + Math.sign(dx) * width;
667
+ const vertY = entity.y + slope * (vertX - entity.x);
668
+
669
+ // 检查与水平边界的交点
670
+ const horizY = entity.y + Math.sign(dy) * height;
671
+ const horizX = entity.x + invSlope * (horizY - entity.y);
672
+
673
+ // 选择最接近实体中心的交点
674
+ const dxVert = Math.abs(vertX - entity.x);
675
+ const dyVert = Math.abs(vertY - entity.y);
676
+ const dVert = Math.sqrt(dxVert * dxVert + dyVert * dyVert);
677
+
678
+ const dxHoriz = Math.abs(horizX - entity.x);
679
+ const dyHoriz = Math.abs(horizY - entity.y);
680
+ const dHoriz = Math.sqrt(dxHoriz * dxHoriz + dyHoriz * dyHoriz);
681
+
682
+ if (dVert <= dHoriz && Math.abs(vertY - entity.y) <= height) {
683
+ intersectX = vertX;
684
+ intersectY = vertY;
685
+ } else if (Math.abs(horizX - entity.x) <= width) {
686
+ intersectX = horizX;
687
+ intersectY = horizY;
688
+ } else {
689
+ // 边缘情况 - 使用角点
690
+ intersectX = entity.x + Math.sign(dx) * width;
691
+ intersectY = entity.y + Math.sign(dy) * height;
692
+ }
693
+ }
694
+
695
+ return { x: intersectX, y: intersectY };
696
+ }
697
+
698
+ // 计算实体到属性的连接点
699
+ function calculateEntityToAttributeConnection(entity, attribute) {
700
+ const dx = attribute.x - entity.x;
701
+ const dy = attribute.y - entity.y;
702
+ const angle = Math.atan2(dy, dx);
703
+
704
+ // 同样的方法,但简化版本
705
+ const width = entity.width / 2;
706
+ const height = entity.height / 2;
707
+
708
+ // 考虑特殊情况
709
+ if (Math.abs(dx) < 0.001) { // 垂直线
710
+ return {
711
+ x: entity.x,
712
+ y: entity.y + Math.sign(dy) * height
713
+ };
714
+ } else if (Math.abs(dy) < 0.001) { // 水平线
715
+ return {
716
+ x: entity.x + Math.sign(dx) * width,
717
+ y: entity.y
718
+ };
719
+ } else {
720
+ // 一般情况
721
+ const slope = dy / dx;
722
+
723
+ // 比较水平和垂直交点
724
+ const tx = width / Math.abs(Math.cos(angle));
725
+ const ty = height / Math.abs(Math.sin(angle));
726
+
727
+ if (tx < ty) {
728
+ return {
729
+ x: entity.x + Math.sign(dx) * width,
730
+ y: entity.y + slope * Math.sign(dx) * width
731
+ };
732
+ } else {
733
+ return {
734
+ x: entity.x + Math.sign(dy) * height / slope,
735
+ y: entity.y + Math.sign(dy) * height
736
+ };
737
+ }
738
+ }
739
+ }
740
+
741
+ // 计算属性到实体的连接点 (用于椭圆)
742
+ function calculateAttributeToEntityConnection(attribute, entity) {
743
+ const dx = entity.x - attribute.x;
744
+ const dy = entity.y - attribute.y;
745
+ const angle = Math.atan2(dy, dx);
746
+
747
+ // 椭圆上的点
748
+ return {
749
+ x: attribute.x + Math.cos(angle) * attribute.width,
750
+ y: attribute.y + Math.sin(angle) * attribute.height
751
+ };
752
+ }
753
+
754
+ // 生成SVG
755
+ function generateSVG(entities, relations, layout, title) {
756
+ // 首先准备SVG容器
757
+ let svgContent = `
758
+ <svg width="${layout.width}" height="${layout.height}" xmlns="http://www.w3.org/2000/svg">
759
+ `;
760
+
761
+ // 添加标题
762
+ svgContent += `
763
+ <text x="${layout.width/2}" y="40" text-anchor="middle" font-size="24" font-weight="bold" fill="#333">${title}</text>
764
+ `;
765
+
766
+ // 创建分层SVG - 确保正确的Z顺序
767
+ // 1. 底层:所有连接线
768
+ let linesLayer = '<g id="lines-layer">\n';
769
+
770
+ // 2. 实体-关系连接线
771
+ for (const [relationName, relationLayout] of Object.entries(layout.relations)) {
772
+ const fromEntity = layout.entities[relationLayout.from.entity];
773
+ const toEntity = layout.entities[relationLayout.to.entity];
774
+
775
+ if (fromEntity && toEntity) {
776
+ // 计算精确的连接点
777
+ const fromConnection = calculateConnectionPoint(fromEntity, relationLayout);
778
+ const toConnection = calculateConnectionPoint(toEntity, relationLayout);
779
+
780
+ // 实体-关系连接线
781
+ linesLayer += `
782
+ <line x1="${fromConnection.x}" y1="${fromConnection.y}" x2="${relationLayout.x}" y2="${relationLayout.y}"
783
+ stroke="#6c757d" stroke-width="2" />
784
+ <line x1="${relationLayout.x}" y1="${relationLayout.y}" x2="${toConnection.x}" y2="${toConnection.y}"
785
+ stroke="#6c757d" stroke-width="2" />
786
+ `;
787
+
788
+ // 基数标记位置 - 距离实体连接点1/4处
789
+ const fromCardX = fromConnection.x + (relationLayout.x - fromConnection.x) * 0.25;
790
+ const fromCardY = fromConnection.y + (relationLayout.y - fromConnection.y) * 0.25;
791
+
792
+ const toCardX = toConnection.x + (relationLayout.x - toConnection.x) * 0.25;
793
+ const toCardY = toConnection.y + (relationLayout.y - toConnection.y) * 0.25;
794
+
795
+ // 基数文本
796
+ linesLayer += `
797
+ <text x="${fromCardX}" y="${fromCardY}" text-anchor="middle" dominant-baseline="middle"
798
+ font-weight="bold" font-size="16" fill="#dc3545">${relationLayout.from.cardinality}</text>
799
+ <text x="${toCardX}" y="${toCardY}" text-anchor="middle" dominant-baseline="middle"
800
+ font-weight="bold" font-size="16" fill="#dc3545">${relationLayout.to.cardinality}</text>
801
+ `;
802
+ }
803
+ }
804
+
805
+ // 实体-属性连接线
806
+ for (const [entityName, entityLayout] of Object.entries(layout.entities)) {
807
+ // 主键连接线
808
+ if (entityLayout.pk) {
809
+ const pk = entityLayout.pk;
810
+
811
+ // 计算精确的连接点
812
+ const entityConnection = calculateEntityToAttributeConnection(entityLayout, pk);
813
+ const pkConnection = calculateAttributeToEntityConnection(pk, entityLayout);
814
+
815
+ linesLayer += `
816
+ <line x1="${entityConnection.x}" y1="${entityConnection.y}" x2="${pkConnection.x}" y2="${pkConnection.y}"
817
+ stroke="#dc3545" stroke-width="1.5" />
818
+ `;
819
+ }
820
+
821
+ // 普通属性连接线
822
+ entityLayout.attributes.forEach(attr => {
823
+ // 计算精确的连接点
824
+ const entityConnection = calculateEntityToAttributeConnection(entityLayout, attr);
825
+ const attrConnection = calculateAttributeToEntityConnection(attr, entityLayout);
826
+
827
+ linesLayer += `
828
+ <line x1="${entityConnection.x}" y1="${entityConnection.y}" x2="${attrConnection.x}" y2="${attrConnection.y}"
829
+ stroke="#28a745" stroke-width="1.5" />
830
+ `;
831
+ });
832
+ }
833
+
834
+ linesLayer += '</g>\n';
835
+
836
+ // 3. 中层:关系菱形
837
+ let relationshipsLayer = '<g id="relationships-layer">\n';
838
+
839
+ for (const [relationName, relationLayout] of Object.entries(layout.relations)) {
840
+ const x = relationLayout.x;
841
+ const y = relationLayout.y;
842
+ const w = relationLayout.width;
843
+ const h = relationLayout.height;
844
+
845
+ relationshipsLayer += `
846
+ <polygon points="${x},${y-h} ${x+w},${y} ${x},${y+h} ${x-w},${y}"
847
+ fill="#fff3cd" stroke="#ffc107" stroke-width="1.5" />
848
+ <text x="${x}" y="${y}" text-anchor="middle" dominant-baseline="middle"
849
+ font-size="14" fill="#212529">${relationName}</text>
850
+ `;
851
+ }
852
+
853
+ relationshipsLayer += '</g>\n';
854
+
855
+ // 4. 上层:实体矩形和属性椭圆
856
+ let entitiesLayer = '<g id="entities-layer">\n';
857
+
858
+ for (const [entityName, entityLayout] of Object.entries(layout.entities)) {
859
+ // 绘制属性椭圆
860
+ // 先画主键
861
+ if (entityLayout.pk) {
862
+ const pk = entityLayout.pk;
863
+ entitiesLayer += `
864
+ <ellipse cx="${pk.x}" cy="${pk.y}" rx="${pk.width}" ry="${pk.height}"
865
+ fill="#ffebee" stroke="#dc3545" stroke-width="2" />
866
+ <text x="${pk.x}" y="${pk.y}" text-anchor="middle" dominant-baseline="middle"
867
+ font-size="14" font-weight="bold">${pk.name}</text>
868
+ `;
869
+ }
870
+
871
+ // 普通属性
872
+ entityLayout.attributes.forEach(attr => {
873
+ entitiesLayer += `
874
+ <ellipse cx="${attr.x}" cy="${attr.y}" rx="${attr.width}" ry="${attr.height}"
875
+ fill="#f8f9fa" stroke="#28a745" stroke-width="1.5" />
876
+ <text x="${attr.x}" y="${attr.y}" text-anchor="middle" dominant-baseline="middle"
877
+ font-size="14">${attr.name}</text>
878
+ `;
879
+ });
880
+
881
+ // 绘制实体矩形
882
+ entitiesLayer += `
883
+ <rect x="${entityLayout.x - entityLayout.width/2}" y="${entityLayout.y - entityLayout.height/2}"
884
+ width="${entityLayout.width}" height="${entityLayout.height}" rx="5" ry="5"
885
+ fill="#e3f2fd" stroke="#0d6efd" stroke-width="2" />
886
+ <text x="${entityLayout.x}" y="${entityLayout.y}" text-anchor="middle" dominant-baseline="middle"
887
+ font-size="16" font-weight="bold" fill="#0d6efd">${entityName}</text>
888
+ `;
889
+ }
890
+
891
+ entitiesLayer += '</g>\n';
892
+
893
+ // 组合所有层级,确保正确的Z轴顺序
894
+ svgContent += linesLayer; // 最底层:连接线
895
+ svgContent += relationshipsLayer; // 中间层:关系菱形
896
+ svgContent += entitiesLayer; // 最上层:实体和属性
897
+
898
+ svgContent += '</svg>';
899
+ return svgContent;
900
+ }
901
+
902
+ // 生成ER图
903
+ function generateERDiagram() {
904
+ const erCode = document.getElementById('erCode').value;
905
+ const title = document.getElementById('diagramTitle').value || 'ER图';
906
+
907
+ try {
908
+ const { entities, relations } = parseERCode(erCode);
909
+ const layout = createLayout(entities, relations);
910
+ const svgContent = generateSVG(entities, relations, layout, title);
911
+
912
+ document.getElementById('svgContainer').innerHTML = svgContent;
913
+ } catch (error) {
914
+ console.error('生成ER图时出错:', error);
915
+ alert('生成ER图时出错: ' + error.message);
916
+ }
917
+ }
918
+
919
+ // 初始加载和事件监听
920
+ document.addEventListener('DOMContentLoaded', function() {
921
+ // 初始生成
922
+ generateERDiagram();
923
+
924
+ // 绑定生成按钮事件
925
+ document.getElementById('generateBtn').addEventListener('click', generateERDiagram);
926
+
927
+ // 绑定标题更改事件
928
+ document.getElementById('diagramTitle').addEventListener('change', generateERDiagram);
929
+ });
930
+ </script>
931
+ </body>
932
+ </html>