Files
FamilyBook/templates/layout.php
T
laoyang ecf121772f v24 第 12 批:5 条(删除按钮位置/日期控件三修/日历跳转/节气节日/财务图)
用户 2026-10-05 的 5 条。第 11 批交付后用户追问"确定这些需求都做完了?"—— 没有,
这 5 条是进一步要求。实现过程中查出 4 个真 bug。

#1 详情页删除按钮紧贴「开通账号」
   上一版放在页面最下方独立区块(danger-zone),用户要「就放在开通账号按钮下面,
   紧挨着,长度大小和开通账号一样,颜色改成红色」。→ 搬进「登录账号」卡片内
   (.del-inline + 细分隔线 + .btn-block-lg)。
   线上几何实测:同一张卡片、左边缘对齐 255==255、宽度一致 990==990、红色 rgb(192,83,63)。

#2 日期控件三修(两个真 bug + 一次闪烁)
   · 灰底:Semi **外层** .semi-datepicker-input 自带 rgba(46,50,56,.05)、高 32px;
     内层 wrapper 白底 40px → 灰的露在外面。实测「灰底元素」= 1 个 → 修后 0 个。
   · 不垂直居中:外层 32px < 内层 40px,实测 gapTop=1 / **gapBottom=17**(文字贴顶)
     → 修后 gapTop=1 / gapBottom=1。
   · 闪烁:实测首 **200ms** 原生输入框可见(要等 3.4MB React 解析)。
     用 no-js/js 二态:<html class="no-js"> + head 内联脚本立刻换 js +
     CSS `html.js .dt-input:not(.is-semi) .dt-native { visibility:hidden }`
     (配同高骨架 ::after 防布局跳动)。实测「出现过原生可见」= False。

#3 日历跳转保留视图 + 下拉日历
   原来写死 view=day(在月视图点跳转会被踢进日视图)。→ 表单带当前 $view(月还带 ym);
   选择器从"居中弹窗"改成**按钮下方下拉** .cal-jump-drop。
   实测:月视图跳 8/15 仍在月视图且是 8 月;周视图跳 8/15 → 落 8/10 那一周;
   日视图跳 8/15 → 显示 8/15。

#4 传统节日 + 二十四节气两个系统本(Schema v18)
   Holiday::SOLAR_TERMS(24 项 C 常数近似公式 + 例外表)+ traditionalRange
   (农历推导;除夕用"次日是否正月初一"反推,写死腊月廿九会有一半年份错)。
   实测 2026:节气 24 条全对(清明 4/5、冬至 12/22);春节 2/17、除夕 2/16、中秋 9/25。
   ⚠️ ENUM 必须显式 ALTER(对已存在的表不生效)——同一坑第二次踩,VERSION 17→18。

#5 财务(一个真 bug + 一个假起点 + 一个真 bug)
   · chart_line 读 `cents` 而 timeline() 返回 `net` → 键名不匹配,**总资产图每点读成 0**
     (正是用户说的"图上是 0")。修:兼容 cents ?? net ?? asset。
     实测 timeline net=800000(8000 元),线上 polyline 有真实起伏。
   · 去掉 timeline 人为插的"前一天=0"假起点(用户:"总资产不应该是 0,
     应该是所有账目创建时的金额的总数")。
   · ppSetSelected 只用 document.querySelector 按名字找,而财务页有**两个** person_id
     组件(「记一笔」+ 修改弹框)→ 永远灌到第一个。加 root 参数、调用方传 panel。

验证:e2e 786/0、交互 92/0、升级 129/0(+3 条 v18 断言)、batch_e 91/0、batch10 46/0、
batch9 38/0、picker_unify 28/0、picker_lazy 11/0、crop 17/0、formbloat 9/0、semi 27/0、
mobile 688/0、drag 5/0、geom 5/5;第 12 批专项:财务 7/0、日历 19/0。
部署静态断言新增 5 组(含反向验证:注入回退 → FAIL=1)。
线上自检 19/19;线上真实账号验收 **30/0**(含真实浏览器几何量删除按钮位置)。
2026-10-04 19:43:00 +08:00

148 lines
8.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// ⚠️ 「系统设置」**不在**顶栏里(用户明确要求):它已经收进右上角头像下拉菜单里了,
// 顶栏留「首页 / 家谱 / 名册 / 观影」四个日常入口,别再把管理入口顶出来。
// ⚠️ v11:「影院」也**不在**顶栏里了(用户明确要求)—— 影院是观影模块的一部分,
// 统一从观影列表页右侧的「设置」按钮进(那里还挂着高级搜索/统计/年度报告)。
// 别再往 $navItems 里加子功能入口,顶栏只放"模块"这一级。
// ⚠️ v14 起导航条**不再写死**:模块清单在 App\Module::LIST 里(加一个模块只改那一处),
// 显示哪些、什么顺序由**每个人自己**的 user_settings('nav_modules') 决定
// (用户第 10 条:允许设置哪些模块出现在导航条上,拖动排序)。
// App\Module::navFor() 内部做了两道兜底(剔除已不存在的模块、补上新装的模块),
// 这里直接拿结果用,不要再自己过滤一遍。
$navKeys = \App\Module::navFor((int)\App\Auth::personId());
$navItems = [];
foreach ($navKeys as $k) {
$navItems[$k] = [\App\Module::name($k), \App\Module::route($k)];
}
// 本页属于哪个模块 → 决定主题色(用户第 11 条:每个模块一个主题色)
$moduleKey = \App\Module::ofPage((string)$pageKey);
if ($moduleKey === '') {
$moduleKey = 'home';
}
$themeVars = \App\Module::cssVars($moduleKey, (int)\App\Auth::familyId());
// ⚠️ 变量名不要叫 $me:layout 是在 extract($data) **之后**被 require 的,
// 这里再赋值会把页面传进来的 $me(当前登录者的完整记录)覆盖掉。
// home.php 用 $me 画「补充亲属关系」链接,被覆盖成 true 之后
// 链接就变成 people/show&id=0,还顺带在页首打出一条 Warning。
// 改名前请先确认所有模板都没有依赖这个布尔值。
$isLoggedIn = \App\Auth::check();
?>
<!DOCTYPE html>
<html lang="zh-CN" class="no-js">
<head>
<?php /* v24 第 12 批 #2:解决「打开页面先闪一下原生输入框,然后才变成 Semi 控件」。
做法是经典的 **no-js / js 二态**:
· 服务端先渲染 class="no-js"(此时原生控件可见 = 无 JS 也能用);
· 下面这段内联脚本**在 <head> 里立即把 class 换成 js** ——
它在 CSS 生效前、任何渲染之前就跑了,所以浏览器**根本没机会**画出原生框;
· CSS 里 `.js .dt-input .dt-native { visibility:hidden }` 先把原生藏住
(用 visibility 而不是 display:none:保留占位高度,避免升级瞬间布局跳动);
· app.js 的 Semi 就绪后加 `.is-semi` 把 Semi 显示出来(原生继续藏着)。
这样"闪烁"从**结构上**消失,而不是靠调加载顺序去掩盖。 */ ?>
<script>document.documentElement.className =
document.documentElement.className.replace('no-js', 'js');</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e($pageTitle) ?> · 家书</title>
<link rel="icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="theme-color" content="#A84A28">
<link rel="stylesheet" href="<?= e(asset('/assets/css/app.css')) ?>">
<?php /* v24 第 9 批 #9:Semi Design DatePicker 懒加载清单(用户点名 semi.design)。
这里只登记带版本号的资源地址,**不加载** —— app.js 只有在页面上真出现
.dt-input(templates/_datetime.php)时才去下这 4 个文件(合计约 4 MB,
绝不能让没有日期控件的页面白拉)。 */ ?>
<meta name="fb-semi" content="<?= e(json_encode([
'css' => asset('/assets/vendor/semi/semi.min.css'),
'react' => asset('/assets/vendor/semi/react.min.js'),
'dom' => asset('/assets/vendor/semi/react-dom.min.js'),
'ui' => asset('/assets/vendor/semi/semi-ui.min.js'),
], JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT)) ?>">
<?php /* v24 第 11 批 #5:Cropper.js(裁剪)也是**按需加载** ——
只有页面上真有 input[type=file][data-crop-ratio] 时才去拉(见 app.js)。
同样在这里登记带版本号的地址,绝不在没有裁剪的页面白拉。 */ ?>
<meta name="fb-cropper" content="<?= e(json_encode([
'css' => asset('/assets/vendor/cropper/cropper.min.css'),
'js' => asset('/assets/vendor/cropper/cropper.min.js'),
], JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT)) ?>">
</head>
<?php /* 主题色:把 --brand / --brand-2 / --brand-ink 三个变量在 <body> 上换成本模块的色,
下面所有用到品牌色的地方(按钮、链接、页头高亮…)自动跟着变。
⚠️ 只覆盖这三个变量,**不碰** --paper / --ink 这些纸墨底色 ——
模块色是用来"分区"的,把整页背景也换了就不是家书了。 */ ?>
<body class="mod-<?= e($moduleKey) ?>"
style="--brand: <?= e($themeVars['brand']) ?>; --brand-2: <?= e($themeVars['brand2']) ?>; --brand-ink: <?= e($themeVars['ink']) ?>;">
<?php if ($isLoggedIn): ?>
<header class="topbar">
<div class="topbar-inner">
<a class="brand" href="<?= e(url('home')) ?>">
<img src="<?= e(asset('/assets/icons/icon-96.png')) ?>" alt="">
<span>家书</span>
</a>
<nav class="nav">
<?php foreach ($navItems as $key => list($label, $route)): ?>
<?php /* 高亮按「本页属于哪个模块」判,而不是 pageKey 全等 ——
子页(观影里的影院管理/统计/年度报告)也要让「观影」亮着。 */ ?>
<a class="<?= $moduleKey === $key ? 'on' : '' ?>" href="<?= e(url($route)) ?>"><?= e($label) ?></a>
<?php endforeach; ?>
</nav>
<div class="topbar-right">
<?php /* 姓名 + 头像 = 一个下拉菜单,而不是「点名字改资料」+旁边两个裸链接。
裸链接在窄屏只能靠 display:none 藏起来(一藏入口就没了),
收进菜单之后同一个入口在手机上也点得到。
用 <details> 实现:不需要 JS 就能展开/收起,app.js 只额外补
"点外面"和"Esc"两件事。 */ ?>
<details class="menu who-menu">
<summary class="who" title="我的">
<img src="<?= e(avatar_url($_SESSION['avatar'] ?? null)) ?>" alt="">
<span class="who-name"><?= e(\App\Auth::name()) ?></span>
<?php if (\App\Auth::isGuest()): ?><span class="badge">访客</span><?php endif; ?>
<i class="caret" aria-hidden="true"></i>
</summary>
<div class="menu-list">
<a href="<?= e(url('settings/me')) ?>">个人设置</a>
<?php if (\App\Auth::isAdmin()): ?>
<a href="<?= e(url('settings')) ?>">系统设置</a>
<?php endif; ?>
<a href="<?= e(url('auth/password')) ?>">修改密码</a>
<a class="menu-exit" href="<?= e(url('auth/logout', ['_csrf' => csrf_token()])) ?>">退出登录</a>
</div>
</details>
</div>
</div>
</header>
<?php endif; ?>
<main class="<?= in_array($pageKey, ['install', 'login'], true) ? 'container-narrow' : 'container' ?>">
<?php foreach (take_flash() as $f): ?>
<div class="flash">
<div class="flash-item <?= $f['type'] === 'err' ? 'err' : '' ?>"><?= e($f['message']) ?></div>
</div>
<?php endforeach; ?>
<?php require $tplFile; ?>
</main>
<?php /* 全局附件预览层(v24 第 3 条):图片 / 视频 / PDF 都在本页这一层里看,
右上角恒有下载按钮。内容由 assets/js/preview.js 按 data-prev 现填。 */ ?>
<div class="pv" id="pv" role="dialog" aria-modal="true" aria-hidden="true" aria-label="附件预览" hidden>
<div class="pv-head">
<span class="pv-name"></span>
<span class="pv-meta"></span>
<a class="pv-dl" href="#">下载</a>
<button type="button" class="pv-close" aria-label="关闭">✕</button>
</div>
<div class="pv-stage"></div>
</div>
<script src="<?= e(asset('/assets/js/app.js')) ?>"></script>
<script src="<?= e(asset('/assets/js/person_picker.js')) ?>"></script>
<script src="<?= e(asset('/assets/js/filter.js')) ?>"></script>
<script src="<?= e(asset('/assets/js/preview.js')) ?>"></script>
</body>
</html>