labull-framework 0.1.3__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {labull_framework-0.1.3/labull_framework.egg-info → labull_framework-0.1.4}/PKG-INFO +1 -1
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/canvas/static/canvas.js +270 -43
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/canvas/static/page.html +6 -2
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/magic_block/deepcheck.py +23 -1
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/magic_block/validate.py +9 -1
- {labull_framework-0.1.3 → labull_framework-0.1.4/labull_framework.egg-info}/PKG-INFO +1 -1
- {labull_framework-0.1.3 → labull_framework-0.1.4}/pyproject.toml +1 -1
- {labull_framework-0.1.3 → labull_framework-0.1.4}/LICENSE +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/MANIFEST.in +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/README-pypi.md +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/__init__.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/admin.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/apps.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/auth/__init__.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/auth/backend.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/auth/bootstrap.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/auth/sso.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/auth/views.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/canvas/__init__.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/canvas/data.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/canvas/gitinfo.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/canvas/static/canvas.css +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/canvas/views.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/env.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/jsonio.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/magic_block/__init__.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/magic_block/devkit.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/magic_block/dispatch.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/magic_block/export.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/magic_block/health.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/magic_block/registry.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/magic_block/sdk.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/magic_block/service.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/magic_block/views.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/management/__init__.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/management/commands/__init__.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/management/commands/labull_contract.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/migrations/0001_initial.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/migrations/__init__.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/models.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/settings.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/urls.py +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework.egg-info/SOURCES.txt +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework.egg-info/dependency_links.txt +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework.egg-info/requires.txt +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework.egg-info/top_level.txt +0 -0
- {labull_framework-0.1.3 → labull_framework-0.1.4}/setup.cfg +0 -0
|
@@ -50,6 +50,11 @@ const GAP = 24;
|
|
|
50
50
|
* (`customer_id` 这种 11 字的只占 82px,宽裕;更长的名字仍会压到卡片,那没办法)。
|
|
51
51
|
* ⚠️ 代价:横向铺得更开 ⇒ 整图更宽 ⇒ "适应视图"的缩放比更小。所以这不是免费的。 */
|
|
52
52
|
const COL_GAP = 140;
|
|
53
|
+
/** 自动排版里"长宽比护栏"的上下限:只最小化总线长会退化成"所有表排成一行",
|
|
54
|
+
* 所以形状偏得太厉害的候选**直接淘汰**,只在"还能看"的那些里比线长。
|
|
55
|
+
* ⚠️ 取 0.45~2.2 是照着一块屏幕的形状(16:9 的宽屏、竖屏手机、以及中间的平板)留的余量。 */
|
|
56
|
+
const ASPECT_MIN = 0.45;
|
|
57
|
+
const ASPECT_MAX = 2.2;
|
|
53
58
|
/** 基数那个圆圈的半径(与 canvas.css 里 `.cardinality circle` 的 `r` 必须一致)。 */
|
|
54
59
|
const MARKER_R = 12;
|
|
55
60
|
/** 两端那支开口箭头(`edgeDefs` 的 marker)在**用户坐标**里有多长:`markerWidth` × `strokeWidth`。
|
|
@@ -167,7 +172,7 @@ async function load() {
|
|
|
167
172
|
notify(`取数失败:${err.message}`, true);
|
|
168
173
|
return;
|
|
169
174
|
}
|
|
170
|
-
layout = { ...(state.layout || {}) };
|
|
175
|
+
layout = { ...(readLocalLayout() || state.layout || {}) };
|
|
171
176
|
notify('');
|
|
172
177
|
|
|
173
178
|
const git = state.git;
|
|
@@ -289,24 +294,13 @@ function render() {
|
|
|
289
294
|
return;
|
|
290
295
|
}
|
|
291
296
|
place(cards, layer);
|
|
292
|
-
/* 🔴
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
* 覆盖成自动排版的结果(那样钉子就不是钉子了);
|
|
299
|
-
* · `?pins=1` 时(拖动 / 钉住用例)才写 —— 那份是"有人选定过的布局",值得留。
|
|
300
|
-
* ⚠️ 只在**真的变了**的时候发:`layoutAtLoad` 是进页面时那份快照,
|
|
301
|
-
* 一样就一个字节都不写 —— 否则每次刷新都往盘上写一次,纯属折腾磁盘。
|
|
302
|
-
* ⚠️ "自动排列"按钮是**故意清空**的(它 POST 一个空对象,见下面那个按钮的处理器)。
|
|
297
|
+
/* 🔴 **自动排版排完不再自动写盘了** —— 现在存不存由用户说了算(右上角「保存排版」按钮,
|
|
298
|
+
* 见 `saveCanvasLayout`),默认每次打开都是自动排版。
|
|
299
|
+
* 上面那段历史的结论就一句:**"存"和"用"都要有明确的开关**,不能靠"排完顺手存一下"
|
|
300
|
+
* 把两边搅在一起(那样用户永远说不清"我这次看到的是排的还是我存的")。
|
|
301
|
+
* ⚠️ 拖动**仍然**每次都存(那是"我正在摆"的直接意图),但**要不要用它**由本地存档决定:
|
|
302
|
+
* 本地存过 ⇒ 用它(见 `LOCAL_KEY`);没存过 ⇒ 每次自动排版。
|
|
303
303
|
*/
|
|
304
|
-
const layoutAtLoad = JSON.stringify(layout);
|
|
305
|
-
for (const el of layer.querySelectorAll('.table')) {
|
|
306
|
-
layout[el.dataset.name] = { x: parseFloat(el.style.left) || 0, y: parseFloat(el.style.top) || 0 };
|
|
307
|
-
}
|
|
308
|
-
const wantPins = new URLSearchParams(location.search).get('pins') === '1';
|
|
309
|
-
if (wantPins && JSON.stringify(layout) !== layoutAtLoad) saveLayout();
|
|
310
304
|
edges(cards, paths, labels);
|
|
311
305
|
// 🔴 把"重画一次"暴露给拖拽:**拖动期间每一帧都要重画**(见 `dragify`)。
|
|
312
306
|
// 原来唯一的重画入口是这一层上的 `click`(下面那一行),而拖动是"按住 + 移动":
|
|
@@ -1242,12 +1236,14 @@ function place(cards, layer) {
|
|
|
1242
1236
|
// 存的坐标是**图层坐标**、以 (0,0) 为原点,所以这个常量偏移对每次渲染都成立。
|
|
1243
1237
|
const top = GAP + loopClearance();
|
|
1244
1238
|
|
|
1245
|
-
// ⚠️
|
|
1246
|
-
|
|
1239
|
+
// ⚠️ 用存下来的位置的两种情形(见 `LOCAL_KEY` 的说明):地址栏显式 `?pins=1`,
|
|
1240
|
+
// 或者**用户在本地存过一份**(点过「保存排版」)—— 存过就说明他要的是那个布局。
|
|
1241
|
+
// 两者都没有 ⇒ 整份 `layout` 都不参与排版,走自动排版(用户要的默认行为)。
|
|
1242
|
+
const wantPins = new URLSearchParams(location.search).get('pins') === '1' || Boolean(readLocalLayout());
|
|
1247
1243
|
const loose = [];
|
|
1248
1244
|
const saved = [];
|
|
1249
1245
|
for (const item of cards) {
|
|
1250
|
-
const spot =
|
|
1246
|
+
const spot = wantPins ? layout[item.el.dataset.name] : null;
|
|
1251
1247
|
if (spot && typeof spot.x === 'number' && typeof spot.y === 'number') {
|
|
1252
1248
|
item.el.style.left = `${spot.x}px`;
|
|
1253
1249
|
item.el.style.top = `${spot.y}px`;
|
|
@@ -1357,21 +1353,201 @@ function placeAuto(items, columns, top, pitch) {
|
|
|
1357
1353
|
// 大的分量先摆:货架排出来更整齐(小分量去补最后那点缝)。
|
|
1358
1354
|
comps.sort((one, two) => two.length - one.length);
|
|
1359
1355
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1356
|
+
/* 🔴 目标里加一条**总线长**(用户要的:「加一个指标确保关联线总的长度尽可能短」)——
|
|
1357
|
+
* 原来只优化"线×线交错",排出来会一路往下堆、线拉得很长,正是用户看到的那个毛病。
|
|
1358
|
+
*
|
|
1359
|
+
* 做法:拿几个候选"列数上限"各排一遍,量**所有关联两端卡片中心的曼哈顿距离之和**,
|
|
1360
|
+
* 取最短的那版写回 DOM。
|
|
1361
|
+
* ⚠️ **必须配长宽比护栏**:只最小化线长会退化成"27 张表排成一行"——线是短了,
|
|
1362
|
+
* 但图变成又扁又宽的一条,`fitView` 缩到看不清,得不偿失。所以长宽比超出
|
|
1363
|
+
* [`ASPECT_MIN`, `ASPECT_MAX`] 的候选**直接淘汰**,只在"形状还能看"的那些里比线长。
|
|
1364
|
+
* ⚠️ 距离用**曼哈顿**(横 + 纵):这一页的走法以横平竖直为主,欧氏距离会把斜线算便宜。
|
|
1365
|
+
* ⚠️ 每排一版都要**真的写进 DOM**:卡片高度是 `offsetHeight` 量出来的(备注能折好几行,
|
|
1366
|
+
* 估不准),所以最后一版会多写一次 DOM 把赢的那版盖回去。
|
|
1367
|
+
* ⚠️ 只在"线长"里比,**不掺交错数**:交错交给上面那步重心排序单独负责,
|
|
1368
|
+
* 两个目标混进一个数里会互相拉扯,反而两个都做不好。
|
|
1369
|
+
*/
|
|
1370
|
+
const run = (limit) => {
|
|
1371
|
+
const box = new Map();
|
|
1372
|
+
let cursor = 0; // 当前货架已经占了几列
|
|
1373
|
+
let shelfTop = top;
|
|
1374
|
+
let shelfTall = 0;
|
|
1375
|
+
for (const members of comps) {
|
|
1376
|
+
const cols = layeringColumns(members, adj, limit);
|
|
1377
|
+
if (cursor + cols.length > limit) {
|
|
1378
|
+
shelfTop += shelfTall + GAP * 2;
|
|
1379
|
+
cursor = 0;
|
|
1380
|
+
shelfTall = 0;
|
|
1381
|
+
}
|
|
1382
|
+
shelfTall = Math.max(shelfTall,
|
|
1383
|
+
placeComponent(cols, adj, byName, box, GAP + cursor * pitch, shelfTop, pitch));
|
|
1384
|
+
cursor += cols.length;
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
const measure = () => {
|
|
1388
|
+
const at = new Map();
|
|
1389
|
+
let right = 0;
|
|
1390
|
+
let bottom = 0;
|
|
1391
|
+
for (const item of items) {
|
|
1392
|
+
const el = item.el;
|
|
1393
|
+
const x = parseFloat(el.style.left) || 0;
|
|
1394
|
+
const y = parseFloat(el.style.top) || 0;
|
|
1395
|
+
at.set(el.dataset.name, { x, y, w: el.offsetWidth, h: el.offsetHeight });
|
|
1396
|
+
right = Math.max(right, x + el.offsetWidth);
|
|
1397
|
+
bottom = Math.max(bottom, y + el.offsetHeight);
|
|
1398
|
+
}
|
|
1399
|
+
let wire = 0;
|
|
1400
|
+
for (const item of items) {
|
|
1401
|
+
const a = at.get(item.el.dataset.name);
|
|
1402
|
+
for (const link of item.table.relations || []) {
|
|
1403
|
+
const b = at.get(link.targetTable);
|
|
1404
|
+
if (!b || b === a) continue;
|
|
1405
|
+
wire += Math.abs((a.x + a.w / 2) - (b.x + b.w / 2)) + Math.abs((a.y + a.h / 2) - (b.y + b.h / 2));
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return { wire: Math.round(wire), w: right, h: bottom, ratio: right / Math.max(1, bottom) };
|
|
1409
|
+
};
|
|
1410
|
+
// 候选:1 列到"视口塞得下的列数",再往上探到"表数一半"(更宽往往更短,但形状会变扁)。
|
|
1411
|
+
const cap = Math.max(2, Math.min(items.length, Math.max(columns, 2) * 3));
|
|
1412
|
+
const tries = [...new Set([columns, Math.max(1, columns - 1), columns + 2, columns * 2, cap])]
|
|
1413
|
+
.filter((k) => k >= 1 && k <= items.length)
|
|
1414
|
+
.sort((one, two) => one - two);
|
|
1415
|
+
const tried = [];
|
|
1416
|
+
let best = null;
|
|
1417
|
+
for (const limit of tries) {
|
|
1418
|
+
run(limit);
|
|
1419
|
+
const m = measure();
|
|
1420
|
+
const ok = m.ratio >= ASPECT_MIN && m.ratio <= ASPECT_MAX;
|
|
1421
|
+
tried.push(`${limit}列:线长${m.wire}${ok ? '' : '(形状淘汰)'}`);
|
|
1422
|
+
if (ok && (!best || m.wire < best.wire)) best = { limit, ...m };
|
|
1423
|
+
}
|
|
1424
|
+
// 一个都不合格(极端形状)时退回原来的列数,绝不把图排成一条线。
|
|
1425
|
+
if (!best) best = { limit: span, ...(run(span), measure()) };
|
|
1426
|
+
if (best.limit !== tries[tries.length - 1]) run(best.limit);
|
|
1427
|
+
|
|
1428
|
+
/* ⑥ 🔴 **线×卡片消解**(用户要的:「完全可以让表疏散开,从而确保线和表不重叠,
|
|
1429
|
+
* 同时还不需要增加线的长度,你为什么没这么做」——他说得对,而且这一步在几何上近乎免费):
|
|
1430
|
+
*
|
|
1431
|
+
* 关键点:**挪一张"中转表"(只是被线穿过、并不是线的两端)完全不改变那条线自己的长度**,
|
|
1432
|
+
* 只可能影响**连在它身上**的其他线。所以判据是"消掉多少穿卡 vs 总线长涨多少"。
|
|
1433
|
+
*
|
|
1434
|
+
* 做法是**贪心 + 有界**,不是全局最优:
|
|
1435
|
+
* · 先算出哪些表被线穿过(只动这些,别的表一律不碰);
|
|
1436
|
+
* · 每张依次试几档纵向平移(上/下 各 40/80/140),
|
|
1437
|
+
* 只接受"**穿卡数下降**、且总线长涨幅 ≤ `MOVE_BUDGET`、且不撞别的卡片"的档;
|
|
1438
|
+
* · 最多 3 轮,没有可接受的挪法就停。
|
|
1439
|
+
* ⚠️ 全程在**内存里算**(`at` 这张表),只有最后赢的位置才写回 DOM —— 不然每一档都要重排 DOM,
|
|
1440
|
+
* 代价是几十倍。
|
|
1441
|
+
* ⚠️ 判据里的"穿过"用**取样**(20 个点):和走线那边用的是同一套朴素判据,
|
|
1442
|
+
* 换来的是"量到的和画出来的一致"。
|
|
1443
|
+
*/
|
|
1444
|
+
const at = new Map();
|
|
1445
|
+
for (const item of items) {
|
|
1446
|
+
const el = item.el;
|
|
1447
|
+
at.set(el.dataset.name, {
|
|
1448
|
+
x: parseFloat(el.style.left) || 0,
|
|
1449
|
+
y: parseFloat(el.style.top) || 0,
|
|
1450
|
+
w: el.offsetWidth,
|
|
1451
|
+
h: el.offsetHeight,
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
const pairs = [];
|
|
1455
|
+
for (const item of items) {
|
|
1456
|
+
const a = item.el.dataset.name;
|
|
1457
|
+
for (const link of item.table.relations || []) {
|
|
1458
|
+
const b = link.targetTable;
|
|
1459
|
+
if (a !== b && at.has(b)) pairs.push([a, b]);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const mid = (spot) => ({ x: spot.x + spot.w / 2, y: spot.y + spot.h / 2 });
|
|
1463
|
+
const hitsRect = (p, q, r) => {
|
|
1464
|
+
for (let i = 1; i < 20; i += 1) {
|
|
1465
|
+
const t = i / 20;
|
|
1466
|
+
const x = p.x + (q.x - p.x) * t;
|
|
1467
|
+
const y = p.y + (q.y - p.y) * t;
|
|
1468
|
+
if (x > r.x && x < r.x + r.w && y > r.y && y < r.y + r.h) return true;
|
|
1469
|
+
}
|
|
1470
|
+
return false;
|
|
1471
|
+
};
|
|
1472
|
+
// 被线穿过的表(返回 名字 → 穿它的线有几条)——只动这些。
|
|
1473
|
+
const scanHits = () => {
|
|
1474
|
+
const bad = new Map();
|
|
1475
|
+
for (const [a, b] of pairs) {
|
|
1476
|
+
const pa = mid(at.get(a));
|
|
1477
|
+
const pb = mid(at.get(b));
|
|
1478
|
+
for (const [name, spot] of at) {
|
|
1479
|
+
if (name === a || name === b) continue;
|
|
1480
|
+
if (hitsRect(pa, pb, spot)) bad.set(name, (bad.get(name) || 0) + 1);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return bad;
|
|
1484
|
+
};
|
|
1485
|
+
const wireOf = () => {
|
|
1486
|
+
let total = 0;
|
|
1487
|
+
for (const [a, b] of pairs) {
|
|
1488
|
+
const pa = mid(at.get(a));
|
|
1489
|
+
const pb = mid(at.get(b));
|
|
1490
|
+
total += Math.abs(pa.x - pb.x) + Math.abs(pa.y - pb.y);
|
|
1370
1491
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1492
|
+
return total;
|
|
1493
|
+
};
|
|
1494
|
+
/* 🔴 判据必须是"**线×卡片的总处数**",不是"被穿过的表有几张":
|
|
1495
|
+
* 一开始用的是 `bad.size`(后者)—— 实测页面上一眼看过去十几处覆盖,它只报 3;
|
|
1496
|
+
* 更糟的是"挪走 A 又压到 B"这种**等价交换**会被判成没进步而拒绝 ⇒ 消解几乎不动
|
|
1497
|
+
* (用户的原话:「还是这么多的覆盖,为什么不舒展开」)。所以改成把每张表被穿几次**加起来**。
|
|
1498
|
+
* ⚠️ 预算与幅度同样放宽(用户授权:"宁可让图更疏散也要消掉"):
|
|
1499
|
+
* 线长涨幅到 35%、纵向挪到 ±400、并允许**横向侧移**(斜线只靠上下躲不开)。 */
|
|
1500
|
+
const totalHits = (m) => [...m.values()].reduce((sum, v) => sum + v, 0);
|
|
1501
|
+
const MOVE_BUDGET = 0.35;
|
|
1502
|
+
const OFFSETS = [
|
|
1503
|
+
[0, -40], [0, 40], [0, -80], [0, 80], [0, -140], [0, 140],
|
|
1504
|
+
[-60, 0], [60, 0], [-120, 0], [120, 0],
|
|
1505
|
+
[0, -220], [0, 220], [0, -320], [0, 320], [0, -400], [0, 400],
|
|
1506
|
+
];
|
|
1507
|
+
let bad = scanHits();
|
|
1508
|
+
const blockHitsBefore = totalHits(bad);
|
|
1509
|
+
let wire0 = wireOf();
|
|
1510
|
+
for (let pass = 0; pass < 6 && totalHits(bad); pass += 1) {
|
|
1511
|
+
let moved = false;
|
|
1512
|
+
for (const name of [...bad.keys()]) {
|
|
1513
|
+
const home = at.get(name);
|
|
1514
|
+
for (const [dx, dy] of OFFSETS) {
|
|
1515
|
+
const trial = { ...home, x: home.x + dx, y: home.y + dy };
|
|
1516
|
+
// 不许撞别的卡片(撞上了就不是"疏散",是"叠在一起")
|
|
1517
|
+
let clash = false;
|
|
1518
|
+
for (const [other, spot] of at) {
|
|
1519
|
+
if (other === name) continue;
|
|
1520
|
+
if (trial.x < spot.x + spot.w && spot.x < trial.x + trial.w
|
|
1521
|
+
&& trial.y < spot.y + spot.h && spot.y < trial.y + trial.h) { clash = true; break; }
|
|
1522
|
+
}
|
|
1523
|
+
if (clash) continue;
|
|
1524
|
+
at.set(name, trial);
|
|
1525
|
+
const next = scanHits();
|
|
1526
|
+
const wire = wireOf();
|
|
1527
|
+
if (totalHits(next) < totalHits(bad) && wire <= wire0 * (1 + MOVE_BUDGET)) {
|
|
1528
|
+
bad = next;
|
|
1529
|
+
moved = true;
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
at.set(name, home);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (!moved) break;
|
|
1536
|
+
}
|
|
1537
|
+
// 只把最终位置写回 DOM(上面全在内存里算)。
|
|
1538
|
+
for (const [name, spot] of at) {
|
|
1539
|
+
const item = byName.get(name);
|
|
1540
|
+
if (!item) continue;
|
|
1541
|
+
item.el.style.left = `${spot.x}px`;
|
|
1542
|
+
item.el.style.top = `${spot.y}px`;
|
|
1374
1543
|
}
|
|
1544
|
+
|
|
1545
|
+
// 给验收 / 排查看:这一版按哪个列数上限排的、总线长多少、试过哪些(含被形状淘汰的)、
|
|
1546
|
+
// 以及"线×卡片消解"最后还剩几处穿卡。
|
|
1547
|
+
window.__labullLayout = {
|
|
1548
|
+
span: best.limit, wire: best.wire, w: best.w, h: best.h, tried,
|
|
1549
|
+
blockHitsBefore, blockHits: totalHits(bad),
|
|
1550
|
+
};
|
|
1375
1551
|
}
|
|
1376
1552
|
|
|
1377
1553
|
/** 分量内部分层:返回"第几列 → 该列的表名(已按重心排好序)"。 */
|
|
@@ -1401,9 +1577,15 @@ function layeringColumns(members, adj, columns) { // 根取**度数最大**的
|
|
|
1401
1577
|
// 视口塞不下、`fitView` 直接顶到最小档 25%,字全糊了(实测:36 张表排出来 4 列、
|
|
1402
1578
|
// 高得离谱,缩放被夹在下限)。这里给每列定个上限,把**超出**的往后挪;
|
|
1403
1579
|
// 挪的是每列**末尾**(重心序里最靠后的),对"关系就近"的破坏最小,挪完再排一遍序。
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1580
|
+
// 🔴 横向预算:**每列最多放 `target` 张**,而 `target` 由**横向预算 `columns`** 算出来。
|
|
1581
|
+
// ⚠️ 原来这里用的是**层数**(`members.length / count`)—— 那等于"横向预算这个参数根本不起作用":
|
|
1582
|
+
// 实测改列数上限(2/3/5/6/9)线长**一模一样**(全是 22824),图始终 2184×3167 又高又窄。
|
|
1583
|
+
// 用户报的「一直往下排版、线拉得很长」就是这个。
|
|
1584
|
+
// ⚠️ 溢出的部分**新增一列**(`splice` 插进去),而不是往后挤:往后挤的话最后一列永远没人接,
|
|
1585
|
+
// 它就会一直堆到天上去。新增列会让后面的列整体右移 —— 那正是"横向发展"。
|
|
1586
|
+
const target = Math.max(1, Math.ceil(members.length / Math.max(1, columns)));
|
|
1587
|
+
for (let c = 0; c < cols.length; c += 1) {
|
|
1588
|
+
while (cols[c].length > target) cols.splice(c + 1, 0, cols[c].splice(target));
|
|
1407
1589
|
}
|
|
1408
1590
|
crossingReduction(cols, adj);
|
|
1409
1591
|
return cols;
|
|
@@ -1804,10 +1986,10 @@ moduleSelect.addEventListener('change', () => {
|
|
|
1804
1986
|
|
|
1805
1987
|
/** 把当前 `layout` 写到服务端。
|
|
1806
1988
|
*
|
|
1807
|
-
* 🔴
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
1989
|
+
* 🔴 两个调用点、一个出口:**拖完一次**要存(原来就存),**点「保存排版」**也要存。
|
|
1990
|
+
* ⚠️ 存不上只是提示,不当成错误:这一页**公开、只读也能看**,位置存不上不该让画布变成空白;
|
|
1991
|
+
* 而且"存到哪"这件事**本地那份(localStorage)才是主力**(见 `LOCAL_KEY`),
|
|
1992
|
+
* 这个 POST 只是"顺便让服务端也留一份"(同一个项目的其他人能看到)。
|
|
1811
1993
|
*/
|
|
1812
1994
|
function saveLayout() {
|
|
1813
1995
|
return fetch('canvas/layout', {
|
|
@@ -1816,16 +1998,61 @@ function saveLayout() {
|
|
|
1816
1998
|
headers: { 'Content-Type': 'application/json', [csrfInfo.header || 'X-CSRFToken']: csrfInfo.token || csrf() },
|
|
1817
1999
|
body: JSON.stringify(layout),
|
|
1818
2000
|
}).then((res) => {
|
|
1819
|
-
if (!res.ok) notify(
|
|
2001
|
+
if (!res.ok) notify(`布局没存到服务端(${res.status})—— 不过本地那份已经存好了,刷新还是这个样子。`, true);
|
|
1820
2002
|
}).catch(() => {});
|
|
1821
2003
|
}
|
|
1822
2004
|
|
|
2005
|
+
/** 本地那份排版的键(存在浏览器里)。
|
|
2006
|
+
*
|
|
2007
|
+
* 🔴 用户要的「点一下就保存当前画布」**主要靠它**:不需要后端能写文件、不需要登录、
|
|
2008
|
+
* 也不需要哪个接口存在 —— 刷新、关掉再开都还在。
|
|
2009
|
+
* 🔴 它同时是"要不要用存下来的位置"的开关:**本地有存档 ⇒ 就不再自动排版**(用户存过就说明
|
|
2010
|
+
* 他要的是这个布局);点「自动排列」会把它删掉,于是回到"每次打开自动排版"。
|
|
2011
|
+
* ⚠️ 用 `try` 包住:隐私模式 / 存储被禁用时 `localStorage` 会直接抛异常,
|
|
2012
|
+
* 那一页还得能看(退回自动排版),不能让画布整个白掉。
|
|
2013
|
+
*/
|
|
2014
|
+
const LOCAL_KEY = 'labull-canvas-layout';
|
|
2015
|
+
|
|
2016
|
+
function readLocalLayout() {
|
|
2017
|
+
try {
|
|
2018
|
+
const parsed = JSON.parse(localStorage.getItem(LOCAL_KEY) || 'null');
|
|
2019
|
+
return parsed && typeof parsed === 'object' && Object.keys(parsed).length ? parsed : null;
|
|
2020
|
+
} catch { return null; }
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
/** 把卡片在 DOM 上的当前位置读回 `layout`(拖动时也是这么更新的)。 */
|
|
2024
|
+
function syncLayoutFromDom() {
|
|
2025
|
+
for (const el of canvas.querySelectorAll('.table')) {
|
|
2026
|
+
layout[el.dataset.name] = { x: parseFloat(el.style.left) || 0, y: parseFloat(el.style.top) || 0 };
|
|
2027
|
+
}
|
|
2028
|
+
return layout;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/** 「保存排版」:存本地(主力)+ 服务端(尽力而为)。 */
|
|
2032
|
+
function saveCanvasLayout() {
|
|
2033
|
+
syncLayoutFromDom();
|
|
2034
|
+
let localOk = false;
|
|
2035
|
+
try {
|
|
2036
|
+
localStorage.setItem(LOCAL_KEY, JSON.stringify(layout));
|
|
2037
|
+
localOk = true;
|
|
2038
|
+
} catch { /* 隐私模式 / 存储被禁用 */ }
|
|
2039
|
+
const count = Object.keys(layout).length;
|
|
2040
|
+
if (localOk) notify(`排版已保存(${count} 张表)—— 之后每次打开都按这个位置显示;要回自动排列点左边的「自动排列」。`);
|
|
2041
|
+
else notify('这个浏览器不让写本地存储,正在试着存到服务端……', true);
|
|
2042
|
+
void saveLayout();
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
$('save-layout').addEventListener('click', saveCanvasLayout);
|
|
2046
|
+
|
|
1823
2047
|
$('reload').addEventListener('click', load);
|
|
1824
2048
|
|
|
1825
2049
|
/** 「自动排列」:清掉手工拖的位置,回到按列自动排。⚠️ 它**不动缩放**(那是「适应视图」的事)——
|
|
1826
|
-
* 两个后果混在一个按钮上时,用户想整理布局却被顺带缩了一下,说不清是谁干的。
|
|
2050
|
+
* 两个后果混在一个按钮上时,用户想整理布局却被顺带缩了一下,说不清是谁干的。
|
|
2051
|
+
* 🔴 它同时要**清掉本地存档**(`LOCAL_KEY`):本地存过一份就"不再自动排版",
|
|
2052
|
+
* 不清的话用户点了「自动排列」、下次打开却又变回他存的那份 —— 按钮就不算数了。 */
|
|
1827
2053
|
$('fit').addEventListener('click', () => {
|
|
1828
2054
|
layout = {};
|
|
2055
|
+
try { localStorage.removeItem(LOCAL_KEY); } catch { /* 隐私模式 */ }
|
|
1829
2056
|
for (const box of canvas.querySelectorAll('.table')) {
|
|
1830
2057
|
box.style.left = '';
|
|
1831
2058
|
box.style.top = '';
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<!-- ⚠️ `?v=` 是**给浏览器的缓存换钥匙**:这两个静态件在"这一页是公开的"之后尤其容易被缓存住,
|
|
8
8
|
改了正文而人不刷新(或刷新了也还是旧的),症状是"代码明明改了、页面却没变"(真踩过)。
|
|
9
9
|
改这两个文件时把版本号往上加一位。 -->
|
|
10
|
-
<link rel="stylesheet" href="canvas/asset/canvas.css?v=
|
|
10
|
+
<link rel="stylesheet" href="canvas/asset/canvas.css?v=36">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<header class="bar">
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
<button id="zoom-fit" type="button" title="把图缩放到视野内">适应视图</button>
|
|
36
36
|
</span>
|
|
37
37
|
<button id="fit" type="button" title="清掉手工拖的位置,回到自动排列">自动排列</button>
|
|
38
|
+
<!-- 🔴 「保存排版」:把当前(拖过的)位置存下来,之后每次打开都用它。
|
|
39
|
+
⚠️ 存两处:**浏览器本地**(一定成功,不要求后端能写文件)+ 后端的 layout 接口
|
|
40
|
+
(尽力而为:这一页是公开的,别的项目里那个 POST 可能压根没人接)。 -->
|
|
41
|
+
<button id="save-layout" type="button" title="保存当前排版:之后每次打开都按这个位置显示(而不是自动排列)">保存排版</button>
|
|
38
42
|
<button id="reload" type="button" title="重新取一次数据">刷新</button>
|
|
39
43
|
</div>
|
|
40
44
|
|
|
@@ -48,6 +52,6 @@
|
|
|
48
52
|
拖动表可排版(拖完自动存);空白处按住拖 = 平移,Ctrl + 滚轮 = 缩放;
|
|
49
53
|
点表标题展开备注与关联,点字段行展开枚举取值 / 文件去向 / 关联目标,点线上的关联键名看它的备注。
|
|
50
54
|
</p>
|
|
51
|
-
<script src="canvas/asset/canvas.js?v=
|
|
55
|
+
<script src="canvas/asset/canvas.js?v=36"></script>
|
|
52
56
|
</body>
|
|
53
57
|
</html>
|
|
@@ -695,12 +695,26 @@ def deep_check(
|
|
|
695
695
|
params = (files or {}).get(PROBE_PARAMS_FILE)
|
|
696
696
|
if isinstance(params, str) and params.strip():
|
|
697
697
|
env[BLOCK_PROBE_PARAMS] = params
|
|
698
|
+
# 🔴 这一段子进程的编码要**两头都钉死**(子进程按 UTF-8 写、父进程按 UTF-8 读):
|
|
699
|
+
# 只写 `text=True` 的话,父进程用 `locale.getpreferredencoding(False)` 解码
|
|
700
|
+
# (中文 Windows = cp936),而子进程一旦吐 UTF-8 —— 服务进程环境里有
|
|
701
|
+
# `PYTHONIOENCODING=utf-8` / `PYTHONUTF8=1` 这类东西时就是这样 —— `subprocess` 的
|
|
702
|
+
# 解码线程会抛 `UnicodeDecodeError` 直接死掉。那个异常在**线程**里,主线程看不见,
|
|
703
|
+
# `stdout`/`stderr` 双双变成 `None` 而 `returncode` 仍是 0 ⇒ 这一层看起来只是
|
|
704
|
+
# "深检自己没跑起来",实则**每一次都没验**(0.1.2 ~ 0.1.3.1 就是这个状态)。
|
|
705
|
+
# 同一个坑的另一处见 `canvas/gitinfo.py` 的 `_run()`。
|
|
706
|
+
env['PYTHONIOENCODING'] = 'utf-8'
|
|
707
|
+
# Windows 商店版 Python 的 `sys.executable` 可能是 None,拿不到就没有解释器可用。
|
|
708
|
+
interpreter = sys.executable or 'python'
|
|
698
709
|
try:
|
|
699
710
|
done = subprocess.run(
|
|
700
|
-
[
|
|
711
|
+
[interpreter, '-c', child],
|
|
701
712
|
env=env,
|
|
702
713
|
capture_output=True,
|
|
703
714
|
text=True,
|
|
715
|
+
encoding='utf-8',
|
|
716
|
+
# 第二道保险:真碰上编码诡异的输出,也宁可显示成问号,不要让解码线程死掉。
|
|
717
|
+
errors='replace',
|
|
704
718
|
timeout=DEEP_TIMEOUT,
|
|
705
719
|
)
|
|
706
720
|
except subprocess.TimeoutExpired:
|
|
@@ -720,6 +734,14 @@ def deep_check(
|
|
|
720
734
|
[],
|
|
721
735
|
)
|
|
722
736
|
|
|
737
|
+
if done.stdout is None:
|
|
738
|
+
# 正常走不到这里(上面 `encoding` / `errors` 已经给全);真走到了也**不许静默**:
|
|
739
|
+
# 至少把 stderr 的最后一行说出来,否则"编码不对 ⇒ 什么都没验"会一直隐形。
|
|
740
|
+
tail = (done.stderr or '').strip().splitlines()
|
|
741
|
+
return [], [
|
|
742
|
+
f'引用深检的输出没能解码(returncode={done.returncode}):'
|
|
743
|
+
f'{tail[-1] if tail else "没有 stderr"}'
|
|
744
|
+
]
|
|
723
745
|
payload = _payload_of(done.stdout or '')
|
|
724
746
|
if payload is None:
|
|
725
747
|
detail = (done.stderr or done.stdout or '').strip().splitlines()
|
|
@@ -488,12 +488,20 @@ def trial_load(src: str) -> list[str]:
|
|
|
488
488
|
# 不交的话每个块都会报"找不到 labull_framework / modules"——那是假错。
|
|
489
489
|
env['PYTHONPATH'] = os.pathsep.join(item for item in sys.path if item)
|
|
490
490
|
env['LABULL_TRIAL_SOURCE'] = json.dumps(src, ensure_ascii=False)
|
|
491
|
+
# 🔴 同 `deepcheck.deep_check()`:子进程按 UTF-8 写、父进程按 UTF-8 读,两头都得钉死。
|
|
492
|
+
# 只写 `text=True` 时父进程按 `locale.getpreferredencoding(False)`(中文 Windows = cp936)
|
|
493
|
+
# 解码,子进程吐 UTF-8 时解码线程会抛 `UnicodeDecodeError` 死掉 ⇒ `stderr` 变成 None,
|
|
494
|
+
# 这一层就只剩一句"加载失败(……):1"(真因被吞了)。
|
|
495
|
+
env['PYTHONIOENCODING'] = 'utf-8'
|
|
496
|
+
interpreter = sys.executable or 'python'
|
|
491
497
|
try:
|
|
492
498
|
done = subprocess.run(
|
|
493
|
-
[
|
|
499
|
+
[interpreter, '-c', _TRIAL_SCRIPT],
|
|
494
500
|
env=env,
|
|
495
501
|
capture_output=True,
|
|
496
502
|
text=True,
|
|
503
|
+
encoding='utf-8',
|
|
504
|
+
errors='replace',
|
|
497
505
|
timeout=TRIAL_LOAD_TIMEOUT,
|
|
498
506
|
)
|
|
499
507
|
except subprocess.TimeoutExpired:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "labull-framework"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Labull Framework: a pluggable Django app with Authing OIDC login and magic blocks"
|
|
9
9
|
readme = "README-pypi.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/management/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labull_framework-0.1.3 → labull_framework-0.1.4}/labull_framework.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|