家谱卡片重做 / 首页控制台 / 名册改列表 / 重复日程与旅程公开

用户 2026-10-03 提的四条:

① 家谱卡片重做
   · 去掉「⇄ 姻亲」按钮,改成**每个人**卡片右下角都有「以 TA 为中心」
     (点了以他重画一张家谱,涵盖原来的姻亲场景)。
   · 头像外框改**长方形**(96×72,宽度贴合头像),object-fit: contain 不裁切,
     姓名/称呼在头像下方。
   · 修「点右下角按钮误进成员页」:卡片不再整张是 <a>,
     只有姓名是链接,右下角是独立的 <button>(物理上分开,不抢点击)。

② 家谱文案与按钮清理
   · 去掉副标题与下方图例说明;整块去掉「看全部 / 回到我 / 回到我的家谱」
     (顺带消掉"出现两个回到我的家谱")。镜头由库自动 fit,拖拽/滚轮即可。

③ 首页控制台
   · 标题下显示个人 slogan;去掉右侧三个统计;「我的资料/我的家谱」换成图标按钮;
     去掉与导航条重复的「名册」按钮。
   · 去掉「接下来的日子」(生日归到"未来七天"里,不再重复)。
   · 新增统计信息块:按时段随机问候 + 今日天气(Open-Meteo,免 key,可留空不显示)
     + 近七天日程/待办/逾期数。
   · 新增各模块小结卡(任务/日程/纪念日/观影/旅程/财务/名册/家谱/资源)。
   · 个人设置新增「首页上的模块」:可勾选与拖动排序(每人一份,存 user_settings)。
   · 保留「往年的今天」。

④ 名册改列表
   · 卡片网格 → **竖向列表**:左头像、中间姓名+与我的关系+手机号+生日。
   · 修拖动「只能往后不能往前」:落点改为显式记录"插到谁前面/后面",
     并排除被拖元素自身。
   · 去掉那句拖动提示(能力保留)。
   · 搜索新增**姓名首字母**检索(y / yy / yyq 都命中「杨永全」):
     新 app/Pinyin.php,intl 优先 + 内置常用字表兜底(本地无 intl 也能测);
     纯字母/数字查询词不走 SQL LIKE(拼音与加密手机号在库里都比不了)。

附带上两轮的收尾:
   · 菜单点外部自动关闭(全局事件委托,覆盖所有 details.menu)。
   · 旅程 is_public:列表与日历只显示公开的;详情页不拦(不丢数据)。
   · 重复日程:新 app/Repeat.php(RRULE 解析/生成/展开,含"工作日"),
     新表 calendar_event_skips(RFC 的 EXDATE);日历按规则展开;
     编辑支持三档「全部 / 这一次及以后 / 仅此一次」。
   · 结束时间可添加/去掉,并随开始时间联动。

验证:端到端 772 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 26、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
This commit is contained in:
2026-10-03 11:30:48 +08:00
parent 8f80f3e835
commit 02f5d072a4
21 changed files with 2235 additions and 213 deletions
+118 -32
View File
@@ -215,31 +215,98 @@ a:hover { text-decoration: underline; }
border-left: 3px solid var(--brand-2);
color: var(--muted); font-size: 14px;
}
/* ⚠️ .hero-stats / .stat 已随用户 2026-10-03 第 3 条撤掉首页那三个统计,
这里保留规则本身(其它页面可能还在用 .stat 这个类名),不再由首页引用。 */
.hero-stats { margin-left: auto; display: flex; gap: 28px; }
.stat { text-align: center; }
.stat b { display: block; font-size: 24px; font-weight: 600; line-height: 1.2; }
.stat span { font-size: 12px; color: var(--muted); }
/* 用户第 3 条:标题下那句个人 slogan */
.hero-slogan { color: var(--muted); margin: 4px 0 0; font-size: 14px; }
/* 用户第 3 条:「我的资料 / 我的家谱」换成**图标按钮**(原来是一排文字按钮)。
margin-left:auto 把它推到最右,与原来统计块的位置一致。 */
.hero-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
.icon-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 40px; height: 40px; border-radius: 12px;
border: 1px solid var(--line); background: var(--surface); color: var(--muted);
transition: border-color .15s ease, color .15s ease, background .15s ease;
}
.icon-btn:hover {
color: var(--brand); border-color: var(--brand-2); background: #FBF7F1;
}
.icon-btn svg { display: block; }
/* ---- 用户第 3 条:首页的统计信息块(问候 + 天气 + 几个数字) ---- */
.home-brief { display: flex; flex-direction: column; gap: 10px; }
.home-brief-hello { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.home-brief-hello b { font-size: 17px; }
.home-brief-weather {
display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
padding: 8px 12px; border-radius: 10px; background: var(--line-2);
}
.home-brief-weather .w-icon { font-size: 20px; line-height: 1; }
.home-brief-weather .w-temp { font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; }
.home-brief-weather .w-desc { font-size: 14px; }
.home-brief-stats { display: flex; flex-wrap: wrap; gap: 10px 22px; color: var(--muted); font-size: 13px; }
.home-brief-stats .hbs b { color: var(--ink); font-size: 16px; margin-right: 3px; }
/* ---- 用户第 3 条:首页的模块小结卡 ---- */
.mod-cards {
display: grid; gap: 12px;
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
}
.mod-card {
display: flex; flex-direction: column; gap: 6px;
padding: 14px 16px; border-radius: var(--radius);
border: 1px solid var(--line); background: var(--surface);
border-left: 3px solid var(--mc, var(--brand));
color: var(--ink); text-decoration: none;
transition: box-shadow .15s ease, border-color .15s ease;
}
.mod-card:hover { box-shadow: var(--shadow); text-decoration: none; }
.mod-card-head { display: flex; align-items: center; gap: 8px; color: var(--mc, var(--brand)); }
.mod-card-icon { display: inline-flex; flex: 0 0 auto; }
.mod-card-name { font-size: 13px; font-weight: 600; }
.mod-card-big { font-size: 20px; font-weight: 600; color: var(--ink); }
.mod-card-lines { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.mod-card-lines li { font-size: 12.5px; color: var(--muted); }
/* ---------------- 成员卡 ---------------- */
/* 用户 2026-10-03 第 4 条:名册从「卡片网格」改成**竖向列表**——
一行一个人,左边头像、中间姓名+关系+手机号/生日、右边操作。
列表形态下拖动排序也更直观(上下移动,不会像网格那样左右跳)。 */
.people-grid {
display: grid; gap: 16px; margin-top: 4px;
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
display: flex; flex-direction: column; gap: 8px; margin-top: 4px;
}
.person {
display: block; color: var(--ink); position: relative;
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); overflow: hidden;
box-shadow: var(--shadow);
transition: transform .15s ease, box-shadow .15s ease;
border-radius: var(--radius-s);
transition: border-color .15s ease, box-shadow .15s ease;
}
.person:hover { transform: translateY(-2px); box-shadow: var(--shadow-l); text-decoration: none; }
.person:hover { border-color: var(--brand-2); box-shadow: var(--shadow); }
/* 头像:列表里用小方图(正方形、cover 裁切),不再是整块大图 */
.person .ph {
background: #F1E7D6; aspect-ratio: 1 / 1; overflow: hidden;
background: #F1E7D6; overflow: hidden;
width: 52px; height: 52px; border-radius: 8px; flex: 0 0 auto;
}
.person .ph img { width: 100%; height: 100%; object-fit: cover; display: block; }
.person .pb { padding: 12px 14px 14px; }
.person .pname { font-weight: 600; font-size: 15px; display: flex; align-items: center; gap: 6px; }
.person .pmeta { color: var(--muted); font-size: 13px; margin-top: 2px; }
.person .person-main {
display: flex; align-items: center; gap: 12px;
padding: 10px 12px; color: var(--ink); text-decoration: none;
}
.person .pb { flex: 1 1 auto; min-width: 0; }
.person .pname { font-weight: 600; font-size: 15px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.person .pmeta { color: var(--muted); font-size: 12.5px; margin-top: 2px; }
/* 中间那几个"重要信息":手机 / 生日 */
.person .pfacts { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 4px; font-size: 13px; }
.person .pfact { color: var(--ink); display: inline-flex; align-items: baseline; gap: 5px; }
.person .pfact-k {
color: var(--muted); font-size: 11px; padding: 1px 5px; border-radius: 4px;
background: var(--line-2);
}
.badge {
display: inline-block; padding: 1px 8px; border-radius: 999px;
@@ -501,12 +568,19 @@ textarea.input { min-height: 76px; resize: vertical; }
.kin-chart .card_cont .card-inner { display: block; }
/* v21:卡片尺寸与 setCardDim({w:140, h:120}) 对齐 */
/* 用户 2026-10-03 第 1 条:
· 头像外框改**长方形**(宽 96 × 高 72),宽度基本就是头像显示宽度;
img 用 object-fit: contain —— 既不裁切,也不会把外框撑变形。
· 信息(姓名/称呼)在头像**下方**。
· 卡片不再整张是链接:只有 .kin-nm 是 <a>,右下角是独立的 <button>,
两者分开,避免"点按钮却进了成员页"的误触。
卡片尺寸与 JS 的 setCardDim({w,h}) 必须一致(库按它算间距与连线落点)。 */
.kin-card {
position: absolute;
display: flex; flex-direction: column; align-items: center;
gap: 0;
width: 140px; height: 120px;
box-sizing: border-box; padding: 4px 4px 3px;
width: 118px; height: 132px;
box-sizing: border-box; padding: 6px 6px 4px;
text-align: center;
background: var(--surface);
border: 1px solid var(--line);
@@ -519,35 +593,36 @@ textarea.input { min-height: 76px; resize: vertical; }
pointer-events: auto; /* 父 .card_cont 是 none,这里恢复 */
}
.kin-card:hover {
box-shadow: var(--shadow-l); border-color: var(--brand-2); text-decoration: none;
}
/* ⚠️ v24 第 12 条:头像是**方形**、不裁切。
卡片高度从 120 加到 136,把这块从 70 放大到 74×74 的正方形;
object-fit 用 contain(不是 cover)—— cover 会把非正方形的头像切掉边,
用户要的正是"所有人的头像都是方形的,别截断"。 */
.kin-card {
width: 148px; height: 138px;
box-shadow: var(--shadow-l); border-color: var(--brand-2);
}
/* 头像外框:**长方形**,宽度贴合头像显示宽度 */
.kin-card .kin-ph {
width: 74px; height: 74px; border-radius: 6px; overflow: hidden;
width: 96px; height: 72px; border-radius: 6px; overflow: hidden;
background: #F1E7D6; flex: 0 0 auto;
display: block;
}
.kin-card .kin-ph img {
width: 100%; height: 100%; object-fit: contain; display: block;
}
/* 「⇄ 姻亲」:卡片右下角的小按钮(这一侧的人没画在本图上时才有) */
.kin-card .kin-branch {
/* 右下角「以 TA 为中心」:每个人都有一个。
⚠️ 单独一个 <button>(不在 <a> 里面)—— 这是"误触"的根治办法:
整卡若是链接,手指偏一点就先触发它跳去成员页了。 */
.kin-card .kin-focus {
position: absolute; right: 3px; bottom: 3px;
font-size: 10px; line-height: 1; padding: 3px 5px; border-radius: 999px;
background: var(--brand); color: #fff; cursor: pointer;
font-size: 10px; line-height: 1; padding: 3px 6px; border-radius: 999px;
border: 0; background: var(--brand); color: #fff; cursor: pointer;
box-shadow: 0 1px 3px rgba(43, 38, 32, .25);
font-family: inherit;
}
.kin-card .kin-branch:hover { background: var(--brand-ink); }
.kin-card .kin-focus:hover { background: var(--brand-ink); }
/* 姓名是唯一的链接入口(进成员资料页) */
.kin-card .kin-nm {
display: block;
font-size: 12.5px; font-weight: 600; line-height: 1.25; max-width: 100%;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
margin-top: 4px;
margin-top: 5px; color: var(--ink); text-decoration: none;
}
.kin-card .kin-nm:hover { color: var(--brand); text-decoration: underline; }
/* 称呼在姓名下面一行,用品牌色的浅字(参考实现是「(称呼)」) */
.kin-card .kin-lb {
font-size: 10.5px; color: var(--brand); line-height: 1.2; max-width: 100%;
@@ -745,18 +820,27 @@ textarea.input { min-height: 76px; resize: vertical; }
/* 卡片改成 div > a + details:表单不能套在链接里(非法 HTML,浏览器会把 DOM 拆开) */
.person-main { display: block; color: var(--ink); }
.person-main:hover { text-decoration: none; }
/* 列表形态下「⋯ 调整分组」改到行的**右侧**(原来在卡片下方,列表里会撑高一行) */
.person-more {
border-top: 1px solid var(--line-2); background: #FCFAF6;
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
border: 0; background: none;
}
.person-more > summary {
list-style: none; cursor: pointer; padding: 7px 14px;
font-size: 12px; color: var(--muted);
list-style: none; cursor: pointer; padding: 4px 10px;
font-size: 16px; color: var(--muted); border-radius: 8px;
}
.person-more > summary:hover { background: var(--line-2); color: var(--brand); }
.person-more > summary::-webkit-details-marker { display: none; }
.person-more > summary::after { content: ' ▾'; }
.person-more[open] > summary::after { content: ' ▴'; }
.person-more > summary:hover { color: var(--brand); }
.person-move { display: flex; gap: 6px; padding: 0 14px 12px; }
.person-more[open] {
position: absolute; right: 8px; top: 8px; transform: none;
background: var(--surface); border: 1px solid var(--line);
border-radius: 10px; box-shadow: var(--shadow-l); z-index: 30;
min-width: 232px; padding-bottom: 4px;
}
.person-move { display: flex; gap: 6px; padding: 8px 12px; flex-wrap: wrap; }
.person-move .select { flex: 1 1 auto; min-width: 0; padding: 6px 8px; font-size: 13px; }
.person-move .btn { flex: 0 0 auto; }
@@ -766,7 +850,7 @@ textarea.input { min-height: 76px; resize: vertical; }
.roster.is-droppable .person { cursor: grab; }
.roster.is-droppable .person.is-dragged { opacity: .45; cursor: grabbing; }
.drag-handle {
position: absolute; left: 8px; top: 8px; z-index: 2;
position: absolute; left: 3px; top: 50%; transform: translateY(-50%); z-index: 2;
width: 22px; height: 22px; line-height: 22px; text-align: center;
border-radius: 6px; font-size: 14px;
color: var(--muted); background: rgba(255, 255, 255, .82);
@@ -774,6 +858,8 @@ textarea.input { min-height: 76px; resize: vertical; }
pointer-events: none; /* 手柄只是个记号,抓取要落在整张卡上 */
}
.drag-handle-inline { color: var(--muted); }
/* 列表里每行要留出左侧拖柄的位置(否则⠿会压在头像上) */
.roster.is-droppable .person { padding-left: 18px; }
/* 空栏平时收起来(七个预置分组可能只用到三个,摊开会显得名册破破烂烂)——
但**拖动时必须露出来**,否则"想拖到一个现在没人的分组里"就没地方放。 */
+78 -8
View File
@@ -546,17 +546,36 @@
e.preventDefault();
if (e.dataTransfer) { e.dataTransfer.dropEffect = 'move'; }
var grid = block.querySelector('.people-grid');
// 落点 = 指针在谁的上半部分,就插到谁前面(和列表拖拽的直觉一致)
var after = null;
/* 落点判定(用户 2026-10-03 第 4 条:"好像只能往后调整,不能往前调整")。
名册已从网格改成**竖向列表**,所以:
指针在某行的**上半部分** → 插到它前面;下半部分 → 插到它后面。
⚠️ 原来只记了"第一个满足 clientY < 中点 的卡片"然后 insertBefore ——
当目标就在**当前拖着的这张之后**时,insertBefore 到它的位置
等于没动(因为 dragged 自己先被移走、后面的元素整体前移一位),
于是"往前拖"看起来完全无效。现在改成显式记录"插到谁前面/后面",
并且在计算时**排除 dragged 自己**,两种方向都成立。 */
var kids = [];
each(grid.querySelectorAll('.person'), function (c) {
if (after || c === dragging) { return; }
var r = c.getBoundingClientRect();
if (e.clientY < r.top + r.height / 2) { after = c; }
if (c !== dragging) { kids.push(c); }
});
if (after) {
grid.insertBefore(dragging, after);
} else {
var target = null; // 落点参照物
var before = true; // 插到它的前面还是后面
for (var i = 0; i < kids.length; i++) {
var r = kids[i].getBoundingClientRect();
if (e.clientY < r.top + r.height / 2) {
target = kids[i];
before = true;
break;
}
target = kids[i];
before = false;
}
if (!target) {
grid.appendChild(dragging);
} else if (before) {
grid.insertBefore(dragging, target);
} else {
grid.insertBefore(dragging, target.nextSibling);
}
});
@@ -574,4 +593,55 @@
save();
});
})();
// ===== 点页面其它地方 / 按 Esc 就收起下拉菜单 =====
// 用户 2026-10-03 第 3 条:「菜单栏点击后面,应该自动关闭,不应该一直保持打开」。
// 原生 <details> 只在再点一次 summary(或按 Esc)时收起 —— 点别处一律不管,
// 于是菜单会"粘"在屏幕上,挡住下面的内容。
//
// 覆盖全站的 details.menu(观影/任务的设置菜单、成员卡片的调整分组…),
// 不给每个页面各写一份。用事件委托:以后新加的菜单自动生效。
(function () {
var openOne = null;
function closeAll(except) {
var forEach = Array.prototype.forEach;
forEach.call(document.querySelectorAll('details.menu[open]'), function (d) {
if (d !== except) { d.open = false; }
});
if (except) { openOne = except; }
}
// toggle 不冒泡,所以用捕获阶段监听,能拿到所有菜单的开合
document.addEventListener('toggle', function (e) {
var d = e.target;
if (!d || d.tagName !== 'DETAILS' || !d.classList.contains('menu')) { return; }
if (d.open) {
closeAll(d); // 同时只留一个开着
} else if (openOne === d) {
openOne = null;
}
}, true);
document.addEventListener('click', function (e) {
if (!openOne) { return; }
// 点在菜单内部(含 summary)→ 不动,交给原生行为
if (openOne.contains(e.target)) { return; }
openOne.open = false;
openOne = null;
});
// Esc 虽然原生也会收,但这里统一收所有(原生只收获得焦点的那一个)
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { closeAll(null); openOne = null; }
});
// 键盘 Tab 走到菜单外时也收起,避免"焦点走了菜单还开着"
document.addEventListener('focusin', function (e) {
if (openOne && !openOne.contains(e.target)) {
openOne.open = false;
openOne = null;
}
});
})();
})();