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) : '·';