Files
TripLog/templates/layout.php
T

185 lines
8.8 KiB
PHP
Raw Normal View History

<?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) : '·';
/* 高德 JS SDK 按需加载(批 30):
只有「首页地图 / 活动详情路线图 / 节点表单地址联想」这三个页面真正用到 AMap 对象,
其余页面(登录、各类管理、节点详情…)一律不加载 —— 省掉 SDK 的下载与解析,
也避免它的两条控制台噪音(WEBGL_debug_renderer_info 弃用提示、
restapi.amap.com 埋点返回 octet-stream 的 MIME 警告)。
⚠️ 新增用 AMap 的模板,必须把相对路径加进下面这个白名单,否则页面会拿不到 SDK。
路径按 `/templates/<相对路径>.php` 匹配(兼容 Windows 反斜杠)。 */
$__needAmap = (bool)preg_match(
'~/templates/(?:activities/(?:index|show)|nodes/form)\.php$~',
str_replace('\\', '/', (string)($content ?? ''))
);
?>
<!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 ?? ((string)(app_config()['app']['name'] ?? '') ?: 'TripLog 家庭旅行日记')) ?></title>
<?php /* 站点图标:一本翻开的日记本 —— 左页「家」、右页「路线 + 图钉」。
ICO 放站点根(浏览器没写地址也会去 /favicon.ico 找),其余走 asset() 带版本号。 */ ?>
<link rel="icon" href="<?= e(asset('favicon.ico')) ?>" sizes="any">
<link rel="icon" type="image/svg+xml" href="<?= e(asset('assets/icons/trip-log.svg')) ?>">
<link rel="apple-touch-icon" href="<?= e(asset('assets/icons/icon-180.png')) ?>">
<link rel="manifest" href="<?= e(asset('site.webmanifest')) ?>">
<meta name="theme-color" content="#3d6bf5">
<?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>
<?php if ($__needAmap): ?>
<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>
<?php endif; ?>
</head>
<body>
<?php if ($__isLoggedIn): ?>
<header class="topbar">
<a href="<?= url('activities/index') ?>" class="brand">
<img class="brand-mark" src="<?= e(asset('assets/icons/trip-log.svg')) ?>" alt="" width="20" height="20">
<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
/* 上一次提交的原始字段(flash_old() 存的)只服务这一次渲染,用完立刻清掉。
不能不清:不清的话「上次失败时填的内容」会一直留在 session 里,
用户下次随便开一个别的表单就会被莫名回填。 */
unset($_SESSION['_old']);
?>
<?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;
}
});
// ===== 时间框:点输入框任意一处都弹出选择器 =====
// 原生行为只在点右侧那个小日历图标时才弹(而且各浏览器还不一致),用户会以为框只能读。
// showPicker() 是标准做法:在 click 里调它,相当于把整块输入区都当成了那个图标。
//
// 为什么能跟浏览器自带的弹窗共存:若浏览器这次点击已经弹过了,再调一次是空操作
// (规范里「show the picker」对已经在显示的选择器直接返回),不会闪两下。
// 不支持的浏览器(老 Safari 等)静默跳过 —— 退化成原生行为,不会比现在更差。
(function () {
var pickers = document.querySelectorAll('input[type="datetime-local"], input[type="date"]');
function hook(el) {
el.addEventListener('click', function () {
if (typeof el.showPicker !== 'function') return;
try {
el.showPicker();
} catch (err) {
// NotAllowedError:不是用户手势触发的(或被跨域 iframe 挡住)。
// 忽略即可 —— 原生那套点击行为还在,用户仍有办法选时间。
}
});
}
for (var i = 0; i < pickers.length; i++) hook(pickers[i]);
})();
</script>
<?php endif; ?>
</body>
</html>