现象:CSS 更新并上线后,用户打开站点看不到任何变化,怀疑没部署。
实测服务器上的 style.css 已是新版(与工作树逐字节一致),真实浏览器
直连线上也拿得到新文件,所以问题在客户端缓存:
· 页面 HTML 带 no-store,每次都重新渲染 → HTML 一直是新的;
· 但 /assets/*.css|js 的响应没有 Cache-Control / Expires,浏览器只好
退回自己的「启发式缓存」——在它自行估算的新鲜期内根本不发请求;
· 而 <link href="/assets/style.css"> 的地址从不改变,文件换了浏览器
也无从得知。于是出现「HTML 新的、CSS 旧的」,界面看起来毫无变化。
修法:新增 asset() 辅助函数,拼上 mtime + 文件大小做 cache busting,
文件名不变而内容一变、地址就变,浏览器只能重新拉取,普通刷新即生效。
版本号同时带上文件大小,挡住「mtime 没变但内容变了」这种漏网情况;
取不到文件信息时不加版本号,退化成原行为,不影响可用性。
已验证:线上 HTML 现在引用 /assets/style.css?v=…;真实浏览器直连线上
三个视口(1920/1280/390)均无横向溢出,资源全部 200;1920px 下顶栏品牌
与正文左边缘同为 420px,头像 26×26(源图 400×400)。
135 lines
5.7 KiB
PHP
135 lines
5.7 KiB
PHP
<?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>
|
||
<?php /* 静态资源一律走 asset():不带版本号的话,浏览器会一直用缓存里的旧 CSS */ ?>
|
||
<link rel="stylesheet" href="<?= e(asset('assets/style.css')) ?>">
|
||
<link rel="stylesheet" href="<?= e(asset('assets/vendor/cropper/cropper.min.css')) ?>">
|
||
<script src="<?= e(asset('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>
|