fix(kin,movies): 家谱连线重构为单一网格 + 观影设置统一页头
需求三点: 1. 观影模块「⚙ 设置」与「记一场电影」按钮高度不一致 → 统一页头动作区 四个选择器的 padding/font-size(.btn 与 .btn-sm 混用是根因);并把设置 下拉推广到观影全部页面(详情/影院/搜索/统计/报告/表单),落在返回按钮旁。 2. 家谱连线「线乱」→ 根因三条:每代各建一个网格(列宽最小值由内容决定, 各代不一致,窄屏实测 93.5px vs 164/78/78/78px);落点用百分比算而 grid 间距不参与百分比;连线行与卡片行不是同一套列。 改法:整棵树只留一个网格,辈分行/卡片行/连线行全是它的行,连线元素本身 也是网格项(靠 justify-self 落位);列宽固定为一卡之宽(不用 minmax/1fr, 否则宽屏卡片中心 ≠ 列中心)。 3. 亲属关系接口改白名单(parent/child/spouse)——原先未知 kind 被静默记成 配偶,夹具传 kind=sibling 让家谱断言长期拿错数据自证。 设置下拉抽成可复用片段 _settings_menu.php($setCurrent 高亮当前页、 $setCanWrite 控「影院管理」显隐),六七个页头不再各抄一份。 验证:e2e 456/0、桌面几何 268/0、移动端几何 504/0、交互 97/0、 家谱连线诊断 0 失败、静态空跑 exit=0、静态断言证伪 13/13、 反向验证 68/68 全抓住。
This commit is contained in:
+61
-30
@@ -123,7 +123,22 @@ a:hover { text-decoration: underline; }
|
|||||||
.page-head { display: flex; align-items: flex-end; gap: 16px; margin-bottom: 20px; }
|
.page-head { display: flex; align-items: flex-end; gap: 16px; margin-bottom: 20px; }
|
||||||
.page-head h1 { font-size: 22px; font-weight: 600; margin: 0; }
|
.page-head h1 { font-size: 22px; font-weight: 600; margin: 0; }
|
||||||
.page-head .sub { color: var(--muted); font-size: 14px; }
|
.page-head .sub { color: var(--muted); font-size: 14px; }
|
||||||
.page-head .actions { margin-left: auto; display: flex; gap: 8px; }
|
.page-head .actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
|
||||||
|
/* ---- 页头动作区:所有按钮一律同尺寸 ----
|
||||||
|
⚠️ 用户明确提过两次:「设置按钮和记一场电影按钮高度要一致,现在不好看」
|
||||||
|
「返回等按钮和记一场电影等按钮的大小高度的统一问题」。
|
||||||
|
根因是页头里混用了 .btn 与 .btn-sm 两档(padding 9px16px vs 5px12px),
|
||||||
|
并排就一高一矮。这里**强制**让页头动作区的按钮回到基础档:
|
||||||
|
· 按钮(.btn / .btn-ghost / .btn-danger,含 <details> 的 summary)统一 padding 与字号;
|
||||||
|
· 表单包裹的按钮(删除那种 <form><button>)也一样 —— 它们才是真正的按钮。
|
||||||
|
为什么用直接子选择器 + 后代 button:页头里只会有这两类交互元素,
|
||||||
|
范围收敛、不会误伤正文里的 .btn-sm(那里小一档是刻意的)。 */
|
||||||
|
.page-head .actions > .btn,
|
||||||
|
.page-head .actions > .movie-set > summary.btn,
|
||||||
|
.page-head .actions > form > .btn,
|
||||||
|
.page-head .actions > form > button.btn {
|
||||||
|
padding: 9px 16px; font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.section { margin-top: 28px; }
|
.section { margin-top: 28px; }
|
||||||
.section-title {
|
.section-title {
|
||||||
@@ -381,8 +396,14 @@ textarea.input { min-height: 76px; resize: vertical; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 家谱(以我为中心) ----------------
|
/* ---------------- 家谱(以我为中心) ----------------
|
||||||
v11 改版:每代是「夫妻单元」网格 + 父母括号连线。
|
v12 改版:**整棵树只有一个 grid**,「辈分行 / 卡片行 / 连线行」都只是它的行。
|
||||||
所有代的列网格共用同一份 template(宽度取最宽那一代),连线才能跟人对齐。 */
|
连线(横杆 .tc-bar / 上引线 .tc-up / 下引线 .tc-down)全是网格项、靠 justify-self 落位,
|
||||||
|
因此「竖线落在哪一列」由浏览器按列算 —— 没有任何 px 或百分比换算,天然对齐。
|
||||||
|
|
||||||
|
⚠️ v11 是「每代各建一个网格」,那是「线乱」的根因:列宽最小值是 minmax(min-content,1fr),
|
||||||
|
由各自的内容决定 —— 有夫妻的那一代最小要「两卡 + 短横」、单身那代只要一卡,
|
||||||
|
于是窄屏实测各代列宽 93.5px vs 164/78/78/78px,同一条竖线上下相差 31~62px。
|
||||||
|
v12 用一份 grid-template-columns 贯穿全树,这个偏差从构造上就不存在。 */
|
||||||
.tree { display: flex; flex-direction: column; align-items: center; gap: 0; }
|
.tree { display: flex; flex-direction: column; align-items: center; gap: 0; }
|
||||||
.tree-inner {
|
.tree-inner {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -390,10 +411,17 @@ textarea.input { min-height: 76px; resize: vertical; }
|
|||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
/* ⚠️ 必须是 min-content 而不是 100%:min-content 让「代」按内容自然展开,
|
.tree-grid {
|
||||||
.tree-inner 才有东西可滚;列的地板由 .tree-row 的 minmax(min-content,1fr) 兜。
|
--col-w: 96px; /* 一列 = 一张卡片宽(窄屏媒体查询里改 78px) */
|
||||||
两处一起才成立 —— 单改一处会在窄屏下把卡片压重叠(v11 实测踩到)。 */
|
--col-gap: 14px; /* 列间距,同时也是夫妻短横的宽度(两处必须一致) */
|
||||||
.tree-gen { min-width: min-content; }
|
display: grid;
|
||||||
|
gap: var(--col-gap);
|
||||||
|
justify-content: center; /* 列宽固定,整棵树居中(家里人少时不左靠) */
|
||||||
|
align-items: start;
|
||||||
|
/* ⚠️ min-width 必须是 min-content:让网格按内容自然展开,.tree-inner 才有东西可滚。
|
||||||
|
写成 100% 的话窄屏会把列一路压到 --col-w 以下,同代卡片就会重叠。 */
|
||||||
|
min-width: min-content;
|
||||||
|
}
|
||||||
.tree-gen-label {
|
.tree-gen-label {
|
||||||
display: flex; align-items: center; gap: 8px;
|
display: flex; align-items: center; gap: 8px;
|
||||||
margin: 0 0 8px; padding-left: 2px;
|
margin: 0 0 8px; padding-left: 2px;
|
||||||
@@ -403,17 +431,17 @@ textarea.input { min-height: 76px; resize: vertical; }
|
|||||||
font-size: 11px; color: var(--muted); background: var(--line-2);
|
font-size: 11px; color: var(--muted); background: var(--line-2);
|
||||||
padding: 1px 8px; border-radius: 999px;
|
padding: 1px 8px; border-radius: 999px;
|
||||||
}
|
}
|
||||||
/* 一代 = 若干「户」,每户占网格的一列(列数由 inline grid-template-columns 给) */
|
/* 一「户」= 网格里的连续若干列(夫妻跨 2 列、单人 1 列;inline 给 grid-column)。
|
||||||
.tree-row {
|
⚠️ width:100% 是必需的:grid 项默认按内容宽(stretch 只在没写 width 时生效),
|
||||||
display: grid; gap: 14px;
|
显式撑满它所占的那几列后,「两张卡 + 一根短横」才能正好落进「两列 + 一个间距」。 */
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
.tree-unit {
|
.tree-unit {
|
||||||
display: flex; align-items: center; justify-content: center; gap: 0;
|
display: flex; align-items: center; justify-content: center; gap: 0;
|
||||||
position: relative; padding: 0 2px;
|
position: relative; width: 100%;
|
||||||
}
|
}
|
||||||
/* 夫妻之间的短横(儿女亲家那种「连着的两个人」) */
|
/* 夫妻之间的短横:宽度**必须等于列间距 --col-gap** —— 这样「同一户内的两个卡」
|
||||||
.tree-bond { width: 14px; flex: 0 0 auto; display: flex; align-items: center; }
|
与「相邻两户的两个卡」看起来是同一个节奏,且跨两列的户宽度正好等于该跨距。 */
|
||||||
|
.tree-bond { width: var(--col-gap); flex: 0 0 auto; display: flex; align-items: center; }
|
||||||
|
/* 夫妻之间的短横(那根"连着的两个人"的横线) */
|
||||||
.tree-bond i {
|
.tree-bond i {
|
||||||
display: block; width: 100%; height: 2px; border-radius: 2px;
|
display: block; width: 100%; height: 2px; border-radius: 2px;
|
||||||
background: var(--line); transform: translateY(2px);
|
background: var(--line); transform: translateY(2px);
|
||||||
@@ -448,24 +476,28 @@ textarea.input { min-height: 76px; resize: vertical; }
|
|||||||
font-size: 10px; color: var(--faint); background: var(--paper);
|
font-size: 10px; color: var(--faint); background: var(--paper);
|
||||||
border: 1px solid var(--line); border-radius: 999px; padding: 0 6px;
|
border: 1px solid var(--line); border-radius: 999px; padding: 0 6px;
|
||||||
}
|
}
|
||||||
/* ---- 父母括号连线 ----
|
/* ---- 父母折线(family elbow)----
|
||||||
.tc-bar = 横杆(占父母列 + 子女各列,居中画一根竖线上去接父母)
|
.tc-bar = 横杆(跨「父母户 → 它的子女们」那几列)
|
||||||
.tc-down = 每个子女列的一道竖线(接进同一个横杆)*/
|
.tc-up = 上引线(落在**父母那一户**的中心,从横杆往上接到父母)
|
||||||
.tree-conn {
|
.tc-down = 下引线(落在**子女本人**那一列的中心,从横杆往下接到子女)
|
||||||
display: grid; gap: 14px; height: 30px; position: relative;
|
⚠️ 三者都是网格项,靠 justify-self 定位,不写任何 left/百分比 —— 这是「线不乱」的关键。 */
|
||||||
|
.tc-bar {
|
||||||
|
align-self: end; justify-self: stretch;
|
||||||
|
height: 30px; position: relative; margin-bottom: -2px;
|
||||||
}
|
}
|
||||||
.tc-bar { position: relative; align-self: end; border-top: 0; }
|
|
||||||
.tc-bar::before {
|
.tc-bar::before {
|
||||||
content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
|
content: ''; position: absolute; left: 0; right: 0; bottom: 0;
|
||||||
background: var(--line); border-radius: 2px;
|
height: 2px; background: var(--line); border-radius: 2px;
|
||||||
}
|
}
|
||||||
.tc-up {
|
.tc-up {
|
||||||
position: absolute; bottom: 0; width: 2px; height: 14px;
|
align-self: end; justify-self: center;
|
||||||
background: var(--line); transform: translateX(-1px);
|
width: 2px; height: 15px; background: var(--line);
|
||||||
|
margin-bottom: 14px; /* 横杆在上引线下方 14px 处,两者接得上 */
|
||||||
}
|
}
|
||||||
.tc-down {
|
.tc-down {
|
||||||
align-self: end; justify-self: center;
|
align-self: start; justify-self: center;
|
||||||
width: 2px; height: 30px; background: var(--line);
|
width: 2px; height: 15px; background: var(--line);
|
||||||
|
margin-top: -2px;
|
||||||
}
|
}
|
||||||
.tree-legend { font-size: 12px; color: var(--muted); margin: 14px 2px 0; }
|
.tree-legend { font-size: 12px; color: var(--muted); margin: 14px 2px 0; }
|
||||||
|
|
||||||
@@ -537,10 +569,9 @@ textarea.input { min-height: 76px; resize: vertical; }
|
|||||||
.rel-form { grid-template-columns: 1fr; }
|
.rel-form { grid-template-columns: 1fr; }
|
||||||
.tnode { width: 78px; padding: 8px 4px 7px; }
|
.tnode { width: 78px; padding: 8px 4px 7px; }
|
||||||
.tnode .tph { width: 40px; height: 40px; }
|
.tnode .tph { width: 40px; height: 40px; }
|
||||||
.tree-row { gap: 8px; }
|
/* 窄屏收窄列宽与列间距:卡宽 78px,与上面 .tnode 的窄版式对齐 */
|
||||||
|
.tree-grid { --col-w: 78px; --col-gap: 8px; }
|
||||||
.tree-unit { padding: 0; }
|
.tree-unit { padding: 0; }
|
||||||
.tree-bond { width: 8px; }
|
|
||||||
.tree-conn { gap: 8px; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
|||||||
@@ -1100,6 +1100,18 @@ switch ($r) {
|
|||||||
// 约定:表单里 from_id 是「当前打开的人」,也就是表单上写的那个「我」;
|
// 约定:表单里 from_id 是「当前打开的人」,也就是表单上写的那个「我」;
|
||||||
// kind=parent 表示「对方是我的父母」→ 所以我是 child、对方是 parent。
|
// kind=parent 表示「对方是我的父母」→ 所以我是 child、对方是 parent。
|
||||||
// Relations::addParent($familyId, $childId, $parentId, $role) 的第二个参数是**孩子**。
|
// Relations::addParent($familyId, $childId, $parentId, $role) 的第二个参数是**孩子**。
|
||||||
|
//
|
||||||
|
// ⚠️⚠️ 这里是**白名单**,不是 else 兜底。原先写成 `else { addSpouse(...) }`,
|
||||||
|
// 于是任何没被识别的 kind(大小写错、拼错、以后新增的种类)都会被**静默记成配偶**
|
||||||
|
// 且照样提示「关系已记录」——关系是错的,用户却以为成功了。
|
||||||
|
// 实测踩到:夹具传 kind=sibling(兄弟姐妹),库里落的全是 spouse,
|
||||||
|
// 于是「夫妻并排」「父母括号」这些断言看着在测,其实测的是一堆错关系。
|
||||||
|
// 兄弟姐妹本就不需要单独记 —— 有同一对父母就会自动认出来(见 Kinship::siblingIds)。
|
||||||
|
$kindMap = ['parent', 'child', 'spouse'];
|
||||||
|
if (!in_array($kind, $kindMap, true)) {
|
||||||
|
flash('err', '不认识这种关系,没有记录。兄弟姐妹不用单独填 —— 有同一对父母就会自动认出来。');
|
||||||
|
redirect('people/show', ['id' => $from]);
|
||||||
|
}
|
||||||
if ($kind === 'parent') {
|
if ($kind === 'parent') {
|
||||||
// 对方是我的父母:我是孩子,对方是父母
|
// 对方是我的父母:我是孩子,对方是父母
|
||||||
$r = Relations::addParent($familyId, $from, $to, $role);
|
$r = Relations::addParent($familyId, $from, $to, $role);
|
||||||
|
|||||||
+92
-71
@@ -1,20 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* 我的家谱:以我为中心,向上向下各三代。**v11 重写** —— 从「按代平铺」改成「按户排布」。
|
* 我的家谱:以我为中心,向上向下各三代。**v12 重写** —— 从「每代各一个网格」改成「全树一个网格」。
|
||||||
*
|
*
|
||||||
* 用户提出的四点,对应这里的四个做法:
|
* 为什么必须重写(v11 实测到的三个真缺陷):
|
||||||
* ① 「每一层级都需要按照长子左、次子右的顺序排序,夫妻要放在一起」
|
* ① **每代各建一个网格 → 各代的列宽可以不一样**。`repeat(N, minmax(min-content,1fr))`
|
||||||
* → 同一代不再平铺一堆卡片,而是先聚成**夫妻单元**(一户),
|
* 的最小值是**内容**决定的:有夫妻的那一代最小要「两卡 + 短横」,单身那代只要一卡。
|
||||||
* 夫妻并排(男左女右、血亲在左),再把单元按长幼从左到右排。
|
* 窄屏(430px)实测:父母辈列宽 93.5px、我这一辈 164/78/78/78px —— 列坐标根本不一样,
|
||||||
* 聚合与排序在 App\Kinship::tree() 里算好(['units']),这里只负责画。
|
* 同一条竖线在上下两代相差 31~62px,线自然全乱。
|
||||||
* ② 「上下代之间要有连线,明确谁是谁的父母与孩子」
|
* ② 横杆上的竖线落点用**百分比**算,而 grid 的列间距不参与百分比 —— 列一多就系统性偏移。
|
||||||
* → 用**父母括号**(family bracket):父母那一户下方画一个横跨它所有子女的
|
* ③ 连线行按「代」切分,与所属的网格**不是同一套列**,无法从几何上保证对齐。
|
||||||
* 方括号,括号中心垂下一根线接到父母,每个子女各自的竖线接进同一根横杆。
|
*
|
||||||
* 谁是谁的父母/孩子因此一眼可辨,而不是"两代之间一条装饰竖线"。
|
* v12 的做法:**整棵树只用一个 grid**(`$colStyle` 挂在 .tree-grid 上),
|
||||||
* ③ 「妹夫、姐夫……谁是姐姐谁是妹妹是很明确的事情」等称呼
|
* 「辈分行 / 卡片行 / 连线行」只是这张网格里的不同行:
|
||||||
* → 全部在 Kinship::label() 里按长幼定死(本模板只渲染结果,不自己做判断)。
|
* · 卡片行:每户占 grid-column 的一段(夫妻占 2 列、单人占 1 列);
|
||||||
* ④ 「小姨子、小姑子、大姨子这种称呼,有错的,需要完全正确」
|
* · 连线行:横杆、上引线、下引线**都是网格项**,各自占自己的列并 `justify-self:center`,
|
||||||
* → 同上;另对「妻子的父母」这类漂在图上的旁支标一句「姻亲」,免得看着像断线。
|
* 于是「竖线落在哪」是浏览器按列算的 —— 不需要任何 px/百分比换算,天然对齐。
|
||||||
|
* 各代的列因此完全一致,横向滚动仍交给 .tree-inner。
|
||||||
|
*
|
||||||
|
* 称呼、姻亲、长幼排序这些语义一个字没改,仍由 App\Kinship::tree() / label() 决定。
|
||||||
*/
|
*/
|
||||||
$gens = $tree['generations'];
|
$gens = $tree['generations'];
|
||||||
$units = $tree['units'] ?? [];
|
$units = $tree['units'] ?? [];
|
||||||
@@ -37,22 +40,45 @@ $genWord = function (int $level): string {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$levels = array_keys($gens);
|
$levels = array_keys($gens);
|
||||||
// 整棵图的宽度取「最宽那一代」—— 所有代共用一个宽度,连线才能跟人对齐
|
|
||||||
$maxUnits = 1;
|
// ---- 把每一代摊成「列位(slot)」 ----
|
||||||
foreach ($levels as $lv) {
|
// 一「户」占几个列位:夫妻 2(并排 + 中间一根短横)、单人 1。
|
||||||
$maxUnits = max($maxUnits, count($units[$lv] ?? []));
|
// 整棵树的列数取最宽那一代;**所有代共用这一套列**,竖线才可能对齐。
|
||||||
|
$slotOf = []; // level => [单元key => ['start'=>起始列位, 'span'=>占几列]]
|
||||||
|
$maxSlots = 1;
|
||||||
|
foreach ($levels as $level) {
|
||||||
|
$s = 0;
|
||||||
|
foreach (($units[$level] ?? []) as $u) {
|
||||||
|
$span = $u['spouse'] !== null ? 2 : 1;
|
||||||
|
$slotOf[$level][$u['key']] = ['start' => $s, 'span' => $span];
|
||||||
|
$s += $span;
|
||||||
|
}
|
||||||
|
// ⚠️ 至少要 1:整棵树只有「我」一个人时也不能写出 repeat(0, …)(那是无效值)
|
||||||
|
$maxSlots = max($maxSlots, max(1, $s));
|
||||||
}
|
}
|
||||||
// ⚠️ 列宽最小值必须是 min-content(**不能**是 0):
|
|
||||||
// v11 实测踩到 —— 最小值给 0 时列会被压到 96px 以下,而卡片是固定 96px 宽,
|
// 列宽**固定 = 一卡之宽**(--col-w:桌面 96 / 窄屏 78),不是 1fr。
|
||||||
// 于是窄屏 + 户数多时同代卡片**互相重叠**(照片与名字糊成一团)。
|
//
|
||||||
// minmax(min-content, 1fr) 给列一个「不小于内容」的地板:内容窄时照旧均分填满,
|
// ⚠️ 为什么必须是固定值而不是 minmax(96px,1fr):列宽给 1fr 时,宽屏下列被撑到 ~195px,
|
||||||
// 内容宽过版心时整棵树交给 .tree-inner 横向滚动(这正是家谱的设计意图)。
|
// 而卡片只有 96px、在户内 flex 居中 —— 于是**卡片中心 ≠ 列中心**。
|
||||||
$colStyle = 'grid-template-columns: repeat(' . (int)$maxUnits . ', minmax(min-content, 1fr));';
|
// 连线靠 justify-self:center 落在**列**中心,就会与卡片错开(实测夫妻户偏 50px)。
|
||||||
|
// 列宽固定成 96px 后:
|
||||||
|
// · 单列户 宽 = 96 = 卡片宽 → 卡片正好居中,中心 = 列中心;
|
||||||
|
// · 夫妻户 宽 = 96+14+96 = 两卡 + 一根短横 → 内容正好填满,两卡各自落在自己的列上。
|
||||||
|
// 两列的间距(gap)与夫妻短横同宽,所以「户内的两个卡」与「跨户的两个卡」间距也一致。
|
||||||
|
// 木桶效应不存在了:列宽恒定 → 卡片永远不会被压到地板以下(重叠问题从构造上消失)。
|
||||||
|
$colStyle = 'grid-template-columns: repeat(' . (int)$maxSlots . ', var(--col-w));';
|
||||||
|
|
||||||
|
// 网格行号:每代占 3 行 —— 「辈分行 / 卡片行 / 连线行」。
|
||||||
|
// 行号写死(而不是靠自动排布)是为了让上引线、横杆、下引线精确落到同一行。
|
||||||
|
$rowLabel = function (int $li): int { return $li * 3 + 1; };
|
||||||
|
$rowCards = function (int $li): int { return $li * 3 + 2; };
|
||||||
|
$rowConn = function (int $li): int { return $li * 3 + 3; };
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1>我的家谱</h1>
|
<h1>我的家谱</h1>
|
||||||
<span class="sub">同代按长幼从左到右 · 夫妻并排 · 括号表示谁是谁的父母</span>
|
<span class="sub">同代按长幼从左到右 · 夫妻并排 · 折线表示谁是谁的父母</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (!$nodes || count($nodes) <= 1): ?>
|
<?php if (!$nodes || count($nodes) <= 1): ?>
|
||||||
@@ -65,18 +91,22 @@ $colStyle = 'grid-template-columns: repeat(' . (int)$maxUnits . ', minmax(min-co
|
|||||||
|
|
||||||
<div class="tree">
|
<div class="tree">
|
||||||
<div class="tree-inner">
|
<div class="tree-inner">
|
||||||
|
<div class="tree-grid" style="<?= e($colStyle) ?>">
|
||||||
<?php foreach ($levels as $li => $level): ?>
|
<?php foreach ($levels as $li => $level): ?>
|
||||||
<?php $levelUnits = $units[$level] ?? []; ?>
|
<?php $levelUnits = $units[$level] ?? []; ?>
|
||||||
|
|
||||||
<div class="tree-gen">
|
<div class="tree-gen-label" style="grid-column: 1 / -1; grid-row: <?= $rowLabel($li) ?>">
|
||||||
<div class="tree-gen-label">
|
|
||||||
<span class="gname"><?= e($genWord((int)$level)) ?></span>
|
<span class="gname"><?= e($genWord((int)$level)) ?></span>
|
||||||
<span class="gtag"><?= count($levelUnits) ?> 户</span>
|
<span class="gtag"><?= count($levelUnits) ?> 户</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tree-row" style="<?= e($colStyle) ?>">
|
|
||||||
<?php foreach ($levelUnits as $u): ?>
|
<?php foreach ($levelUnits as $u): ?>
|
||||||
<div class="tree-unit">
|
<?php
|
||||||
|
$sl = $slotOf[$level][$u['key']];
|
||||||
|
$unitStyle = 'grid-column: ' . ($sl['start'] + 1) . ' / span ' . $sl['span']
|
||||||
|
. '; grid-row: ' . $rowCards($li) . ';';
|
||||||
|
?>
|
||||||
|
<div class="tree-unit" style="<?= e($unitStyle) ?>">
|
||||||
<?php foreach ($u['ids'] as $k => $id): ?>
|
<?php foreach ($u['ids'] as $k => $id): ?>
|
||||||
<?php if ($k === 1): ?>
|
<?php if ($k === 1): ?>
|
||||||
<span class="tree-bond" aria-label="夫妻"><i></i></span>
|
<span class="tree-bond" aria-label="夫妻"><i></i></span>
|
||||||
@@ -96,68 +126,59 @@ $colStyle = 'grid-template-columns: repeat(' . (int)$maxUnits . ', minmax(min-co
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
// ---- 父母括号:这一代(父母)↔ 下一代(子女)的连线 ----
|
// ---- 连线行:这一代(父母)↔ 下一代(子女)----
|
||||||
//
|
//
|
||||||
// 画法(每个「父母户 + 它的子女们」一组):
|
// 每个「父母户 + 它的子女们」画一架**折线**(family elbow):
|
||||||
// · 一根横杆盖住「父母那一列 → 它的子女各列」;
|
// · 横杆盖住「父母列 → 它的子女各列」;
|
||||||
// · 横杆在**父母列的中心**向上伸一根竖线,接到父母那一户;
|
// · 上引线从横杆中点往上接到父母那一户(落在父母**整户的中心列**上);
|
||||||
// · 横杆在**每个子女列的中心**向下伸一根短线,接到各自的子女户。
|
// · 每个子女各一根下引线,接进同一根横杆(落在**血亲本人那一列**的中心)。
|
||||||
// 于是「谁是谁的父母与孩子」是几何上可读的,而不是两代之间一条装饰线。
|
// 三样都是网格项、都靠 justify-self:center 定位 —— 落点由浏览器按列算,必然对齐。
|
||||||
$childLevel = $levels[$li + 1] ?? null;
|
$childLevel = $levels[$li + 1] ?? null;
|
||||||
if ($childLevel !== null && !empty($units[$childLevel])):
|
if ($childLevel !== null && !empty($units[$childLevel])):
|
||||||
$kids = $units[$childLevel];
|
// 父母户key => 它的子女们的列位起点
|
||||||
$ranges = []; // 父母单元key => [第一个子女列, 最后一个子女列]
|
$kidStarts = [];
|
||||||
foreach ($kids as $i => $ku) {
|
foreach ($units[$childLevel] as $ku) {
|
||||||
$pk = $ku['parent_key'];
|
$pk = $ku['parent_key'];
|
||||||
if ($pk === null) {
|
if ($pk !== null && isset($slotOf[$level][$pk])) {
|
||||||
continue;
|
$kidStarts[$pk][] = $slotOf[$childLevel][$ku['key']]['start'];
|
||||||
}
|
|
||||||
if (!isset($ranges[$pk])) {
|
|
||||||
$ranges[$pk] = [$i, $i];
|
|
||||||
} else {
|
|
||||||
$ranges[$pk][1] = $i;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 父母单元key => 它在上一代里的列号(拿来决定竖线落点)
|
$bars = [];
|
||||||
$pidx = [];
|
$downs = [];
|
||||||
foreach (($units[$level] ?? []) as $i => $pu) {
|
foreach ($kidStarts as $pk => $starts) {
|
||||||
$pidx[$pu['key']] = $i;
|
$ps = $slotOf[$level][$pk];
|
||||||
|
$lo = min($ps['start'], min($starts));
|
||||||
|
$hi = max($ps['start'] + $ps['span'] - 1, max($starts));
|
||||||
|
$bars[] = ['lo' => $lo, 'hi' => $hi, 'pStart' => $ps['start'], 'pSpan' => $ps['span'],
|
||||||
|
'who' => ($unitName[$pk]['head'] ?? '') . (($unitName[$pk]['spouse'] ?? null) !== null ? ' 夫妇' : '')];
|
||||||
|
foreach ($starts as $cs) {
|
||||||
|
$downs[] = $cs;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
sort($downs);
|
||||||
?>
|
?>
|
||||||
<div class="tree-conn" style="<?= e($colStyle) ?>">
|
<?php foreach ($bars as $b): ?>
|
||||||
<?php foreach ($ranges as $pk => $rg): ?>
|
<?php $span = $b['hi'] - $b['lo'] + 1; ?>
|
||||||
<?php
|
<div class="tc-bar" style="grid-column: <?= $b['lo'] + 1 ?> / span <?= $span ?>; grid-row: <?= $rowConn($li) ?>"
|
||||||
$pn = $unitName[$pk] ?? ['head' => '', 'spouse' => null];
|
title="<?= e($b['who'] . ' 的子女') ?>"></div>
|
||||||
$who = $pn['head'] . ($pn['spouse'] !== null ? ' 夫妇' : '');
|
<div class="tc-up" title="<?= e($b['who']) ?>"
|
||||||
$pCol = $pidx[$pk] ?? $rg[0];
|
style="grid-column: <?= $b['pStart'] + 1 ?> / span <?= $b['pSpan'] ?>; grid-row: <?= $rowConn($li) ?>"></div>
|
||||||
$lo = min($pCol, $rg[0]);
|
|
||||||
$hi = max($pCol, $rg[1]);
|
|
||||||
$span = $hi - $lo + 1;
|
|
||||||
// 竖线落在父母列的中心(在横杆这一段里的百分比位置)
|
|
||||||
$pct = ($pCol - $lo + 0.5) / $span * 100;
|
|
||||||
?>
|
|
||||||
<div class="tc-bar" style="grid-column: <?= $lo + 1 ?> / <?= $hi + 2 ?>; grid-row: 1"
|
|
||||||
title="<?= e($who . ' 的子女') ?>">
|
|
||||||
<span class="tc-up" style="left: <?= round($pct, 3) ?>%"></span>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php foreach ($kids as $i => $ku): ?>
|
<?php foreach ($downs as $cs): ?>
|
||||||
<div class="tc-down" style="grid-column: <?= $i + 1 ?>; grid-row: 1"></div>
|
<div class="tc-down" style="grid-column: <?= $cs + 1 ?>; grid-row: <?= $rowConn($li) ?>"></div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="tree-legend">
|
<p class="tree-legend">
|
||||||
「姻亲」= 因为婚姻进到这张图里的人(如我妻子的父母),不是本家的血亲。
|
「姻亲」= 因为婚姻进到这张图里的人(如我妻子的父母),不是本家的血亲。
|
||||||
括号横跨的那几户,就是括号上方那一户的子女。
|
折线横跨的那几户,就是它上方那一户的子女;连线从**血亲本人**头上接出。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 观影模块的「⚙ 设置」下拉 —— 影院管理 / 高级搜索 / 统计 / 年度报告。
|
||||||
|
*
|
||||||
|
* 为什么抽成片段:v11 这个下拉只长在观影列表页一个地方;用户随后要求
|
||||||
|
* 「设置这个按钮,在观影详情等页面也都要出现,就出现在返回等按钮的旁边」。
|
||||||
|
* 与其把同一段 <details> 复制到六七个页头(改一处就得改六处、迟早漏),
|
||||||
|
* 不如让所有观影页 require 同一个片段。
|
||||||
|
*
|
||||||
|
* 用法(include 前 set 好):
|
||||||
|
* $setCurrent 当前页的 key('list' | 'cinemas' | 'search' | 'stats' | 'report' | 'show' | 'form')
|
||||||
|
* —— 用来高亮菜单里对应的那一项。空串表示都不高亮。
|
||||||
|
* $setCanWrite 是否显示「影院管理」(只有能写的人才有管理入口)
|
||||||
|
*
|
||||||
|
* ⚠️ 高亮用 `a.on`(.movie-set .menu-list a.on 已有样式)。**不做「隐藏当前页那一项」**:
|
||||||
|
* 隐藏会让菜单在不同页面长短不一,用户更难认出「这是同一套入口」。
|
||||||
|
*/
|
||||||
|
$setCurrent = (string)($setCurrent ?? '');
|
||||||
|
$setCanWrite = (bool)($setCanWrite ?? false);
|
||||||
|
$setOn = static function (string $key) use ($setCurrent): string {
|
||||||
|
return $setCurrent === $key ? ' class="on"' : '';
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
<details class="movie-set menu">
|
||||||
|
<summary class="btn btn-ghost" title="观影设置">⚙ 设置</summary>
|
||||||
|
<div class="menu-list">
|
||||||
|
<?php if ($setCanWrite): ?>
|
||||||
|
<a href="<?= e(url('movies/cinemas')) ?>"<?= $setOn('cinemas') ?>>影院管理</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a href="<?= e(url('movies/search')) ?>"<?= $setOn('search') ?>>高级搜索</a>
|
||||||
|
<a href="<?= e(url('movies/stats')) ?>"<?= $setOn('stats') ?>>统计</a>
|
||||||
|
<a href="<?= e(url('movies/report')) ?>"<?= $setOn('report') ?>>年度报告</a>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<?php
|
||||||
|
// 用完即清,避免残留在后续 include 里(与 _datetime.php 同一纪律)
|
||||||
|
unset($setCurrent, $setCanWrite, $setOn);
|
||||||
|
?>
|
||||||
@@ -18,7 +18,8 @@ $canWrite = \App\Auth::canWrite();
|
|||||||
<h1>影院管理</h1>
|
<h1>影院管理</h1>
|
||||||
<span class="sub">共 <?= count($cinemas) ?> 家影院</span>
|
<span class="sub">共 <?= count($cinemas) ?> 家影院</span>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies')) ?>">返回观影记录</a>
|
<?php $setCurrent = 'cinemas'; $setCanWrite = $canWrite; require __DIR__ . '/_settings_menu.php'; ?>
|
||||||
|
<a class="btn btn-ghost" href="<?= e(url('movies')) ?>">返回观影记录</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ $selAttendees = array_map('intval', (array)old('attendees', $curAttendees));
|
|||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1><?= $isEdit ? '编辑观影记录' : '记一场电影' ?></h1>
|
<h1><?= $isEdit ? '编辑观影记录' : '记一场电影' ?></h1>
|
||||||
<span class="sub"><?= $isEdit ? e($m['title']) : '片名必填,其他都可以以后再补' ?></span>
|
<span class="sub"><?= $isEdit ? e($m['title']) : '片名必填,其他都可以以后再补' ?></span>
|
||||||
|
<div class="actions">
|
||||||
|
<?php /* 用户要求「设置这个按钮,在观影详情等页面也都要出现,就出现在返回等按钮的旁边」。
|
||||||
|
表单页没有返回按钮(提交/取消在表单底部),设置就放在页头动作区。 */ ?>
|
||||||
|
<?php $setCurrent = 'form'; $setCanWrite = \App\Auth::canWrite(); require __DIR__ . '/_settings_menu.php'; ?>
|
||||||
|
<a class="btn btn-ghost"
|
||||||
|
href="<?= e($isEdit ? url('movies/show', ['id' => (int)$m['id']]) : url('movies')) ?>">
|
||||||
|
<?= $isEdit ? '返回详情' : '返回观影' ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" enctype="multipart/form-data"
|
<form method="post" enctype="multipart/form-data"
|
||||||
|
|||||||
@@ -26,19 +26,11 @@ $fmtNum = static function ($v): string {
|
|||||||
<?php if ($canWrite): ?>
|
<?php if ($canWrite): ?>
|
||||||
<a class="btn" href="<?= e(url('movies/new')) ?>">记一场电影</a>
|
<a class="btn" href="<?= e(url('movies/new')) ?>">记一场电影</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php /* v11:右侧的「设置」按钮 —— 影院管理与从 MovieLog 迁来的
|
<?php /* 右侧的「⚙ 设置」—— 影院管理与从 MovieLog 迁来的
|
||||||
高级搜索 / 统计 / 年度报告都收在这里(顶栏不再单放「影院」)。 */ ?>
|
高级搜索 / 统计 / 年度报告都收在这里(顶栏不再单放「影院」)。
|
||||||
<details class="movie-set menu">
|
⚠️ 片段里的按钮**不带 btn-sm**:页头动作区必须同高,见 app.css 的
|
||||||
<summary class="btn btn-ghost btn-sm" title="观影设置">⚙ 设置</summary>
|
「页头动作统一尺寸」一节(用户明确要求这两个按钮高度一致)。 */ ?>
|
||||||
<div class="menu-list">
|
<?php $setCurrent = 'list'; $setCanWrite = $canWrite; require __DIR__ . '/_settings_menu.php'; ?>
|
||||||
<?php if ($canWrite): ?>
|
|
||||||
<a href="<?= e(url('movies/cinemas')) ?>">影院管理</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<a href="<?= e(url('movies/search')) ?>">高级搜索</a>
|
|
||||||
<a href="<?= e(url('movies/stats')) ?>">统计</a>
|
|
||||||
<a href="<?= e(url('movies/report')) ?>">年度报告</a>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ $fmtDate = static function (?string $d): string {
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies')) ?>">返回观影</a>
|
<?php $setCurrent = 'report'; $setCanWrite = \App\Auth::canWrite(); require __DIR__ . '/_settings_menu.php'; ?>
|
||||||
|
<a class="btn btn-ghost" href="<?= e(url('movies')) ?>">返回观影</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
<h1>高级搜索</h1>
|
<h1>高级搜索</h1>
|
||||||
<span class="sub">组合多个条件精确查找</span>
|
<span class="sub">组合多个条件精确查找</span>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies')) ?>">返回观影</a>
|
<?php $setCurrent = 'search'; $setCanWrite = \App\Auth::canWrite(); require __DIR__ . '/_settings_menu.php'; ?>
|
||||||
|
<a class="btn btn-ghost" href="<?= e(url('movies')) ?>">返回观影</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -143,15 +143,17 @@ $commentBox = static function (int $reviewId, int $parentId, string $summary) us
|
|||||||
<span class="sub">观影记录</span>
|
<span class="sub">观影记录</span>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<?php if ($canWrite): ?>
|
<?php if ($canWrite): ?>
|
||||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies/edit', ['id' => (int)$m['id']])) ?>">编辑</a>
|
<a class="btn btn-ghost" href="<?= e(url('movies/edit', ['id' => (int)$m['id']])) ?>">编辑</a>
|
||||||
<form method="post" action="<?= e(url('movies/destroy')) ?>"
|
<form method="post" action="<?= e(url('movies/destroy')) ?>"
|
||||||
onsubmit="return confirm('删除这条观影记录?海报、票根、照片也会一并删除,不可恢复。')">
|
onsubmit="return confirm('删除这条观影记录?海报、票根、照片也会一并删除,不可恢复。')">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="id" value="<?= (int)$m['id'] ?>">
|
<input type="hidden" name="id" value="<?= (int)$m['id'] ?>">
|
||||||
<button type="submit" class="btn btn-danger btn-sm">删除</button>
|
<button type="submit" class="btn btn-danger">删除</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies')) ?>">返回列表</a>
|
<?php /* 设置入口要出现在「返回等按钮的旁边」(用户原话) */ ?>
|
||||||
|
<?php $setCurrent = 'show'; $setCanWrite = $canWrite; require __DIR__ . '/_settings_menu.php'; ?>
|
||||||
|
<a class="btn btn-ghost" href="<?= e(url('movies')) ?>">返回列表</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ $wd = static function (?string $d): string {
|
|||||||
<h1>观影统计</h1>
|
<h1>观影统计</h1>
|
||||||
<span class="sub">一家人的观影概览</span>
|
<span class="sub">一家人的观影概览</span>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('movies')) ?>">返回观影</a>
|
<?php $setCurrent = 'stats'; $setCanWrite = \App\Auth::canWrite(); require __DIR__ . '/_settings_menu.php'; ?>
|
||||||
|
<a class="btn btn-ghost" href="<?= e(url('movies')) ?>">返回观影</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user