Files
TripLog/templates/layout.php
T
laoyang a43dd06d1f 修版心双重内缩与预览图溢出:宽屏整页错位、头像盖住表单
线上反馈「界面更乱了、全都错位、导航条头像非常大、下拉菜单乱七八糟」。
用无头浏览器实测出三处硬伤,都不是样式文件坏了,是布局算式写错。

一、版心被推两次 → 宽屏正文列塌陷(「全都错位」的根)

  .topbar 与 .container 各自用了同一份「把内容推到居中」的 padding,
  而 .container 本来就已经有 max-width + margin:auto。于是内容被推两次:

    1280px  顶栏品牌 x=89   正文 x=178     差 89px
    1920px  顶栏品牌 x=409  正文 x=818     差 409px,正文列只剩 262px
    2560px  正文列宽度 = 0                内容完全挤没

  改法:抽一个 --shell-pad(= max(--gutter, (100% - --container) / 2)),
  顶栏与正文共用它,并且 .container 不再设 max-width —— 正文列宽度由 padding
  反推,宽屏下正好等于 --container,两边用的是同一个公式,左边缘必然对齐。
  用 100% 而非 100vw,避免滚动条让两者差半格。

二、预览容器把图片尺寸写在了外层 div 上 → 400×400 头像盖住整个表单

  .avatar-preview / .cover-preview 把 width/height/object-fit 写在**容器**上,
  里面的 <img> 完全不受约束,按原始尺寸铺出来:
  头像 400×400、封面 1920×1080,直接把下面的表单项盖住。
  (对照:.card-cover img / .activity-cover img 是写对的。)

  改法:容器只负责「框」(定尺寸、overflow:hidden 裁圆角),
  尺寸与 object-fit 一律落到 > img 上,且用绝对定位铺满 ——
  不用 `display:grid + width:100%`,那种写法列宽会反过来依赖元素自身宽度,
  形成循环,浏览器只能把图算成 0 宽(实测踩到过)。

三、上传区被当成小圆框 → 按钮和提示文字压进头像里

  .avatar-upload 被套上 84×84 圆形尺寸,而模板把预览、按钮、提示文字都放在
  它内部,三样东西全挤在那个小圆里互相重叠。改法:上传容器只做排列
  (头像横排、封面竖排),虚线圆框与圆角归预览块,按钮自成一行。

顺带
  · 下拉面板加 max-height + overflow,13 个菜单项在矮屏不再顶出屏幕底部;
    宽度 max-content 且不换行,并限制不超过视口。
  · 头像地址一律先过 media_avatar() 再判断:外站地址(https://…)会被拒,
    以前按原始值判断非空 → 渲染出 <img src="">,用户管理列表里两个用户
    就是破图。现在退回首字母色块。头像隐藏域仍保留原始值,避免单纯保存就抹掉数据。
  · CSS 结构体检报告不再写在被检查文件旁边(assets/style.css.syntax.txt
    曾被当成站点文件打进部署包);打包排除规则改为「根目录 _ 开头 +
    任意层级的工具报告/日志」,并且不再误伤 templates/ 下 _ 前缀的局部模板。

配套的检查能力(本次之前的检查全绿、页面却视觉上是坏的,所以补这一层)
  · _tooling/_snapshot3.py   按登录态渲染线上页面 + 抓静态资源到本地
  · _tooling/_layout_check.py  无头浏览器测量真实几何并断言:顶栏/正文左右边缘
    对齐、正文列宽度、无横向溢出、头像恒 26px、预览图不溢出且铺满、预览图
    确实加载成功、下拉面板不出屏。6 个页面 × 7 档宽度 = 392 项。
    先用线上那份坏样式跑证明它抓得到(89 项失败),再用修复后的跑(0 失败)。
  · _tooling/css_syntax_check.py  词法级 CSS 结构体检(跳过注释与字符串后数括号),
    比 grep 数大括号可靠 —— 配平不等于合法。
  · 部署脚本新增 7 条静态哨兵,锁住上面三个根因。

验证
  本地:php -l 55 文件 0 失败;模板渲染 234 项;SQL 15 表无未知列;PDO 0 问题;
        CSS 202 类全有定义;CSS 结构体检通过;Mime 27 项;内联 JS 9 个文件;
        布局几何 392 项全通过。
  线上:部署 10 步 EXIT=0(80 项静态断言全 OK / 0 FAIL,含 7 条新哨兵),
        双 PHP 渲染 14 条路由字节数完全一致,26 项内容断言全通过
        (含照片代理 69/69 字节逐字节一致);
        对已部署状态重抓渲染再测布局几何 392 项 0 失败;
        端到端 239 项通过 / 0 项失败。
        用户管理列表里 example.com 外链出现次数由 2 降为 0。
2026-09-23 00:01:05 +08:00

134 lines
5.6 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
$__isLoggedIn = \App\Auth::check();
$__user = $__isLoggedIn ? \App\Auth::user() : null;
$__isAdmin = $__isLoggedIn && \App\Auth::isAdmin();
$__name = (string)($__user['name'] ?? ($_SESSION['user_name'] ?? ''));
// 用 media_avatar() 解析后再判断,不能拿原始值判断:
// 外站地址(https://…)会被 media_avatar() 拒掉返回空串,若还按非空处理,
// 就会渲染出 <img src=""> —— 一个破图占位。
$__avatar = media_avatar((string)($__user['avatar'] ?? ''));
$__initial = $__name !== '' ? mb_substr($__name, 0, 1) : '·';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title><?= e($pageTitle ?? '家庭出行记录') ?></title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="stylesheet" href="/assets/vendor/cropper/cropper.min.css">
<script src="/assets/vendor/cropper/cropper.min.js"></script>
<script>
window._AMapSecurityConfig = {
securityJsCode: '<?= e(app_config()['amap']['security_js'] ?? '') ?>',
};
window.__AMAP_LOADED = false;
window.__AMAP_ERROR = null;
</script>
<script src="https://webapi.amap.com/maps?v=2.0&key=<?= e(app_config()['amap']['key'] ?? '') ?>&plugin=AMap.AutoComplete"
onload="window.__AMAP_LOADED = true;"
onerror="window.__AMAP_ERROR = 'script load failed';">
</script>
</head>
<body>
<?php if ($__isLoggedIn): ?>
<header class="topbar">
<a href="<?= url('activities/index') ?>" class="brand">
<span class="brand-mark">🧭</span>
<span class="brand-text">家庭出行记录</span>
</a>
<div class="topbar-right">
<a href="<?= url('activities/create') ?>" class="btn-primary btn-sm topbar-new">+ 新建活动</a>
<?php /* 原生 details 做下拉:不依赖 JS,键盘可用,点外部失焦由 JS 兜底 */ ?>
<details class="menu" id="user-menu">
<summary class="menu-trigger" aria-label="打开菜单">
<?php if ($__avatar !== ''): ?>
<img class="avatar-xs" src="<?= e($__avatar) ?>" alt="">
<?php else: ?>
<span class="avatar-xs avatar-initial"><?= e($__initial) ?></span>
<?php endif; ?>
<span class="menu-name"><?= e($__name) ?></span>
<span class="menu-caret" aria-hidden="true">▾</span>
</summary>
<div class="menu-panel">
<div class="menu-head">
<div class="menu-head-name"><?= e($__name) ?></div>
<div class="menu-head-role"><?= $__isAdmin ? '管理员' : '家庭成员' ?></div>
</div>
<a class="menu-item" href="<?= url('activities/index') ?>">
<span class="menu-icon">📋</span> 活动列表
</a>
<a class="menu-item" href="<?= url('activities/create') ?>">
<span class="menu-icon">➕</span> 新建活动
</a>
<?php if (\App\Auth::id()): ?>
<a class="menu-item" href="<?= url('users/edit', ['id' => \App\Auth::id()]) ?>">
<span class="menu-icon">🙋</span> 我的资料
</a>
<?php endif; ?>
<?php if ($__isAdmin): ?>
<div class="menu-sep">管理</div>
<a class="menu-item" href="<?= url('travel_modes/index') ?>">
<span class="menu-icon">🚄</span> 出行方式
</a>
<a class="menu-item" href="<?= url('vehicles/index') ?>">
<span class="menu-icon">🚗</span> 车辆
</a>
<a class="menu-item" href="<?= url('node_types/index') ?>">
<span class="menu-icon">🏷️</span> 节点类型
</a>
<a class="menu-item" href="<?= url('role_tags/index') ?>">
<span class="menu-icon">🎭</span> 角色标签
</a>
<a class="menu-item" href="<?= url('users/index') ?>">
<span class="menu-icon">👥</span> 用户管理
</a>
<a class="menu-item" href="<?= url('settings/index') ?>">
<span class="menu-icon">🗄️</span> 存储设置
</a>
<?php endif; ?>
<div class="menu-sep"></div>
<a class="menu-item menu-item-danger" href="<?= url('auth/logout') ?>">
<span class="menu-icon">🚪</span> 退出登录
</a>
</div>
</details>
</div>
</header>
<?php endif; ?>
<main class="container<?= $__isLoggedIn ? '' : ' container-guest' ?>">
<?php if ($err = flash('error')): ?>
<div class="alert"><?= e($err) ?></div>
<?php endif; ?>
<?php require $content; ?>
</main>
<?php if ($__isLoggedIn): ?>
<footer class="footer">家庭出行记录</footer>
<script>
// 原生 details 不会因为点击面板外部而收起,这里补一句(不依赖它也能用)
document.addEventListener('click', function (ev) {
var menu = document.getElementById('user-menu');
if (menu && menu.open && !menu.contains(ev.target)) {
menu.open = false;
}
});
// Esc 收起
document.addEventListener('keydown', function (ev) {
var menu = document.getElementById('user-menu');
if (ev.key === 'Escape' && menu && menu.open) {
menu.open = false;
}
});
</script>
<?php endif; ?>
</body>
</html>