diff --git a/assets/style.css b/assets/style.css
index 46b60ea..c03beb0 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -42,12 +42,30 @@
--shadow-md: 0 8px 24px rgba(22, 28, 45, .10);
--shadow-lg: 0 18px 48px rgba(22, 28, 45, .16);
- --container: 1080px;
+ /* ── 版心 ─────────────────────────────────────────────────────────────
+ 顶栏与正文必须共用同一套「左右留白」,否则宽屏下两者左边缘会错开。 */
+ --container: 1080px; /* 正文列的目标宽度 */
+ --gutter: clamp(14px, 3.2vw, 24px); /* 窄屏时内容与视口边缘的最小留白 */
--topbar-h: 60px;
+
+ /* 版心左右留白:视口比 --container 宽时,把它推到「让正文列正好等于
+ --container 且居中」的位置;视口不够宽时退化为 --gutter。
+ ⚠️ 顶栏与 .container 只共用这一个值,两边都不许再叠加
+ `max-width + margin auto` —— 那等于把内容推两次。曾经就是这么错的:
+ 1920px 下正文列只剩 240px,2560px 下直接塌成 0,整页错位。
+ ⚠️ 这里用 100% 而不是 100vw:100vw 含滚动条宽度,会让顶栏比正文
+ 偏出半个滚动条。100% 相对各自包含块,两边算出来才严格相等。 */
+ --shell-pad: max(var(--gutter), calc((100% - var(--container)) / 2));
}
* { box-sizing: border-box; }
+/* 图片一律不得宽过所在容器 —— 这是兜底,不是主要手段。
+ 主因见下面 .avatar-preview / .cover-preview 的注释:尺寸要写在 img 上。
+ (这里只约束宽度,高度仍由各自的规则决定;单靠这条救不了「400×400 的方图
+ 塞进 84px 圆框」那种高度溢出。) */
+img { max-width: 100%; }
+
html { -webkit-text-size-adjust: 100%; }
body {
@@ -92,7 +110,7 @@ hr { border: 0; border-top: 1px solid var(--border); margin: 20px 0; }
display: flex; align-items: center; justify-content: space-between;
gap: 12px;
min-height: var(--topbar-h);
- padding: 8px max(14px, calc((100vw - var(--container)) / 2));
+ padding: 8px var(--shell-pad); /* 与 .container 同一个版心留白 */
background: rgba(255, 255, 255, .88);
backdrop-filter: saturate(180%) blur(12px);
-webkit-backdrop-filter: saturate(180%) blur(12px);
@@ -101,15 +119,18 @@ hr { border: 0; border-top: 1px solid var(--border); margin: 20px 0; }
.brand {
display: inline-flex; align-items: center; gap: 8px;
+ min-width: 0; /* 允许品牌文字省略,别把顶栏撑破 */
font-weight: 680; font-size: 16px; color: var(--text); letter-spacing: -.01em;
}
.brand:hover { text-decoration: none; color: var(--text); }
.brand-mark { font-size: 18px; line-height: 1; }
+.brand-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.topbar-right { display: flex; align-items: center; gap: 10px; }
+/* 右侧一整组(新建 + 菜单)整体不许被压缩:窄屏宁可省略名字,也别把菜单挤没 */
+.topbar-right { display: flex; align-items: center; gap: 10px; flex: none; min-width: 0; }
/* ── 3. 右上角下拉菜单 ───────────────────────────────────────────────────── */
-.menu { position: relative; }
+.menu { position: relative; flex: none; }
.menu-trigger {
list-style: none;
@@ -138,7 +159,15 @@ hr { border: 0; border-top: 1px solid var(--border); margin: 20px 0; }
.menu-panel {
position: absolute; right: 0; top: calc(100% + 8px);
- min-width: 216px;
+ /* 宽:按最长菜单项撑开(不换行),但窄屏下不许宽过视口 */
+ width: max-content;
+ min-width: 224px;
+ max-width: min(320px, calc(100vw - var(--gutter) * 2));
+ /* 高:13 个菜单项接起来有 500 多 px,笔记本(768 高)会顶出屏幕底部。
+ 给上限 + 滚动,菜单项再多也不会「乱七八糟」。 */
+ max-height: calc(100vh - var(--topbar-h) - 24px);
+ overflow-y: auto;
+ overscroll-behavior: contain;
padding: 6px;
background: var(--surface);
border: 1px solid var(--border);
@@ -161,6 +190,7 @@ hr { border: 0; border-top: 1px solid var(--border); margin: 20px 0; }
padding: 8px 10px;
border-radius: var(--radius-sm);
color: var(--text); font-size: 14px; text-decoration: none;
+ white-space: nowrap; /* 菜单项不折行,靠面板宽度自适应 */
transition: background .12s;
}
.menu-item:hover { background: var(--bg-soft); text-decoration: none; color: var(--text); }
@@ -174,9 +204,14 @@ hr { border: 0; border-top: 1px solid var(--border); margin: 20px 0; }
/* ── 4. 容器与页头 ───────────────────────────────────────────────────────── */
.container {
- max-width: var(--container);
+ /* 这里**故意不写 max-width**:正文列宽度由 --shell-pad 反推 ——
+ 视口够宽时 padding 会把它挤到正好 var(--container) 宽且居中;
+ 这与 .topbar 用的是同一个公式,所以两者左边缘必然对齐。
+ 曾经这里把 max-width 也设成了 --container,再配 `margin: 0 auto` 和同一份
+ padding:等于把内容推两次。1920px 下正文列只剩 240px、2560px 下塌成 0,
+ 是本次「整页错位」的直接原因。改这里前请先想清楚这一点。 */
margin: 0 auto;
- padding: 24px max(14px, calc((100vw - var(--container)) / 2)) 64px;
+ padding: 24px var(--shell-pad) 64px;
}
.container-guest {
max-width: 420px;
@@ -760,42 +795,92 @@ select { cursor: pointer; }
.crop-container { width: 100%; max-height: 60vh; overflow: hidden; background: #111; border-radius: var(--radius-sm); }
.crop-container-wide { max-height: 66vh; }
-.crop-container img { max-width: 100%; display: block; }
+/* Cropper 需要按原始尺寸接管这张图(自己算内联宽高),不能被 max-width 夹住;
+ 容器自带 overflow:hidden,所以就算图超出也只会被裁掉,不会撑破页面。 */
+.crop-container img { max-width: none; display: block; }
+/* 预览容器只负责「框」:定尺寸、圆角、把图片裁进去。
+ ⚠️ width / height / object-fit 必须写在 **img** 上,不能写在外层 div 上 ——
+ 写错地方图片就完全不受约束,会按原始尺寸铺出来:头像 400×400、封面
+ 1920×1080,直接把下面的表单盖住(这就是「头像非常大、整页错位」的根)。
+ 对照写对了的 .card-cover img / .activity-cover img。 */
.avatar-preview {
+ position: relative; flex: none;
width: 84px; height: 84px; border-radius: 50%;
- object-fit: cover; background: var(--bg-soft); border: 1px solid var(--border);
+ overflow: hidden; /* 把方图裁成圆,并兜住任何溢出 */
+ background: var(--bg-soft); border: 2px dashed var(--border-strong);
}
.cover-preview {
- width: 100%; aspect-ratio: 16 / 9; height: auto;
- border-radius: var(--radius); object-fit: cover; background: var(--bg-soft);
- border: 1px solid var(--border);
+ position: relative;
+ width: 100%; aspect-ratio: 16 / 9;
+ overflow: hidden;
+ border-radius: var(--radius); background: var(--bg-soft);
+ border: 2px dashed var(--border-strong);
}
-.avatar-placeholder, .avatar-upload {
+/* 图和占位块都用绝对定位铺满容器:
+ 百分比相对 padding box 解析,宽高一定确定。
+ ⚠️ 不要用 `display: grid; place-items: center` + `width: 100%` ——
+ 自动列宽会反过来依赖元素自身宽度,形成循环,浏览器只能把图算成 0 宽
+ (实测就是这么坏的:img 宽 0、只剩一个空框)。 */
+.avatar-preview > img,
+.cover-preview > img {
+ position: absolute; inset: 0;
+ display: block; width: 100%; height: 100%; object-fit: cover;
+}
+.avatar-preview > .avatar-placeholder,
+.cover-preview > .cover-placeholder {
+ position: absolute; inset: 0;
+ width: 100%; height: 100%;
display: flex; align-items: center; justify-content: center;
- width: 84px; height: 84px; border-radius: 50%;
+}
+/* 上传区只是「预览 + 操作按钮」的布局容器,不是那个虚线圆框本身。
+ ⚠️ 这里曾经把 84×84 的圆形尺寸套在 .avatar-upload 上,而模板把预览、
+ 按钮、提示文字都放在它**内部** → 三样东西全被塞进那个小圆里互相重叠
+ (编辑用户页的「选择图片/移除」就压在头像上)。圆框属于
+ .avatar-preview / .cover-preview,容器只负责排列。 */
+.avatar-upload {
+ display: flex; align-items: center; gap: 16px;
+ width: auto; height: auto;
+ border: 0; border-radius: 0; background: none;
+}
+.cover-upload {
+ display: flex; flex-direction: column; gap: 12px;
+ width: 100%; height: auto;
+ border: 0; border-radius: 0; background: none;
+}
+
+/* "+" 占位块:尺寸已经被 `.avatar-preview > .avatar-placeholder` 铺满了,这里只管样子 */
+.avatar-placeholder {
+ border-radius: 50%;
background: var(--bg-soft); color: var(--text-mute); font-size: 26px;
}
-.avatar-upload, .cover-upload {
- border: 2px dashed var(--border-strong); cursor: pointer;
- transition: border-color .15s, background .15s;
+
+/* 悬停高亮落在预览框上(那才是"会被换掉的东西") */
+.avatar-preview, .cover-preview { transition: border-color .15s, background .15s; }
+.avatar-upload:hover .avatar-preview,
+.cover-upload:hover .cover-preview { border-color: var(--accent); background: var(--accent-soft); }
+
+.avatar-upload-actions, .cover-upload-actions {
+ display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; margin-top: 0;
}
-.avatar-upload:hover, .cover-upload:hover { border-color: var(--accent); background: var(--accent-soft); }
-.cover-upload {
- display: flex; align-items: center; justify-content: center;
- width: 100%; aspect-ratio: 16 / 9; border-radius: var(--radius);
- color: var(--text-mute); font-size: 14px;
-}
-.avatar-upload-actions, .cover-upload-actions { display: flex; gap: 8px; margin-top: 10px; align-items: center; }
/* ── 16. 头像与用户 ──────────────────────────────────────────────────────── */
-.avatar-xs {
- width: 26px; height: 26px; border-radius: 50%;
- object-fit: cover; flex: none;
+/* 这两类是直接写在
(或首字母 )上的,所以尺寸放这里是对的。
+ 注意别把 .avatar-placeholder 的 84px 带进来:它常与 .avatar-sm 叠用
+ (用户列表里用首字母当头像),靠下面那条组合规则把字号压回 13px。 */
+.avatar-xs, .avatar-sm {
+ border-radius: 50%; object-fit: cover; flex: none;
display: inline-flex; align-items: center; justify-content: center;
+ overflow: hidden;
}
+.avatar-xs { width: 26px; height: 26px; }
+.avatar-sm { width: 30px; height: 30px; }
.avatar-initial { background: var(--accent); color: #fff; font-size: 12px; font-weight: 620; }
-.avatar-sm { width: 30px; height: 30px; border-radius: 50%; object-fit: cover; flex: none; }
+.avatar-sm.avatar-placeholder,
+.avatar-xs.avatar-placeholder {
+ font-size: 13px; font-weight: 620;
+ background: var(--accent-soft); color: var(--accent);
+}
.user-cell { display: flex; align-items: center; gap: 9px; }
.user-info { display: inline-flex; align-items: center; gap: 8px; }
@@ -906,12 +991,15 @@ select { cursor: pointer; }
@media (max-width: 640px) {
body { font-size: 14.5px; }
- .container { padding: 16px 14px 52px; }
+ /* 留白仍走 --shell-pad(此时它等于 --gutter):断点里也不能改成固定值,
+ 否则窄屏下顶栏与正文又会对不齐。 */
+ .container { padding: 16px var(--shell-pad) 52px; }
.container-guest { padding: 20px 14px; }
- .topbar { padding: 8px 14px; }
+ .topbar { padding: 8px var(--shell-pad); }
.brand-text { display: none; }
.menu-name { display: none; }
.menu-trigger { padding: 4px; gap: 4px; }
+ .menu-panel { min-width: 208px; }
.two-col { grid-template-columns: 1fr; }
.card-grid, .photo-grid, .attachment-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
diff --git a/templates/layout.php b/templates/layout.php
index d21d6ce..74d837f 100644
--- a/templates/layout.php
+++ b/templates/layout.php
@@ -3,7 +3,10 @@ $__isLoggedIn = \App\Auth::check();
$__user = $__isLoggedIn ? \App\Auth::user() : null;
$__isAdmin = $__isLoggedIn && \App\Auth::isAdmin();
$__name = (string)($__user['name'] ?? ($_SESSION['user_name'] ?? ''));
-$__avatar = (string)($__user['avatar'] ?? '');
+// 用 media_avatar() 解析后再判断,不能拿原始值判断:
+// 外站地址(https://…)会被 media_avatar() 拒掉返回空串,若还按非空处理,
+// 就会渲染出
—— 一个破图占位。
+$__avatar = media_avatar((string)($__user['avatar'] ?? ''));
$__initial = $__name !== '' ? mb_substr($__name, 0, 1) : '·';
?>
@@ -42,7 +45,7 @@ $__initial = $__name !== '' ? mb_substr($__name, 0, 1) : '·';