一个基于 PHP + JSON 的个人引导页/导航站,自带 PHP 后台管理面板。无需手动修改 HTML,即可维护分类、卡片、图片、JS 特效、看板娘、加载页主题、本地背景兜底、UA 伪装、公告、全站统计代码、站点显示配置,并支持在线一键升级与回滚。
- 下载源码,上传到 PHP 网站根目录
- 访问
https://你的域名/admin,跟随 4 步安装向导完成设置(约 2 分钟) - 登录后台,在「站点设置」里改标题、头像
- 之后想升级,点击后台顶部栏的「版本号(如 v1.1.0)」,即可进入在线升级页(需服务器支持在线覆盖代码)
无需数据库、无需 Composer、无需构建工具。支持 PHP 7.3+。
当前版本:1.1.1(版本号定义在 admin/defaults.php 的 NUOMI_VERSION 常量,后台顶部栏会自动显示)
覆盖新版本代码前,必须保留以下文件(它们是您的私有数据):
| 保留 | 说明 |
|---|---|
admin/data.json |
卡片、分类、站点配置、公告等全部数据 |
admin/.admin.conf |
管理员账号 |
admin/.update/ |
在线升级缓存、更新包、代码备份、升级状态 |
photos/ |
所有上传的图片(含头像、图标、看板娘、本地背景) |
assets/js/ |
上传的 JS 特效 |
assets/preloaders/ |
上传的加载页主题 |
其余文件(*.php / *.css / *.js / *.md)可直接覆盖。
点击后台顶部栏的「版本号(如 v1.0.0)」胶囊按钮 → 进入 admin/update.php 独立升级页:
- 自动检查更新:对接 GitHub Releases API,5 分钟本地缓存(避免触发 GitHub 60 次/小时速率限制),用
version_compare比较版本号 - 环境检测:自动测试 PHP 运行用户能否在项目根目录创建 / 覆盖 / 删除文件
- ✅ 支持 → 显示「⬆ 立即升级」按钮
- ❌ 不支持(共享主机 / 文件所有者与 PHP 运行用户不一致)→ 显示「🔗 去 GitHub 下载」按钮,跳转 Releases 页面手动覆盖
- 下载更新包:从 Release 的
assets数组里找第一个.zip,自动 SHA256 校验(如果 Release 提供了digest),解压到admin/.update/package/ - 应用升级:
- 先把当前代码备份到
admin/.update/backup/<时间戳>/ - 逐个覆盖,跳过保留清单里的所有路径
- 失败自动回滚
- 成功后写入升级历史
- 先把当前代码备份到
- 一键回滚:随时用最近一次备份覆盖当前代码(
state.json转为rolled_back)
在线升级绝不动这些路径(即使 Release 包里带了也不覆盖):
admin/data.jsonadmin/.admin.confadmin/.update/photos/assets/js/assets/preloaders/README.mdLICENSECHANGELOG.md
在线升级的其他保护措施:
- 路径穿越防护:拒绝
..、绝对路径、Windows 盘符 - 下载地址白名单:只认
github.com/raw.githubusercontent.com/objects.githubusercontent.com/codeload.github.com - 覆盖白名单:仅允许覆盖
admin/、assets/、index.php、404.php、announcement.php、analytics.php - 解压后总大小上限:200MB(防 zip 炸弹)
- 并发锁:
flock(LOCK_EX | LOCK_NB),防止两次升级同时进行 - 状态原子写入:
state.json/history.json用 tmp + rename 原子写入 - 卡死自愈:
applying状态超过 10 分钟自动转failed
如果服务器不支持在线升级(环境检测未通过),或您想完全手动控制:
- 下载新版本压缩包(到项目 Releases 页面)
- 解压后覆盖上传(跳过上面「保留」清单中的文件)
- 访问
/admin刷新,后台顶部栏会显示新的版本号
提示:共享主机常见的「不支持在线升级」原因是 PHP 运行用户(如
www-data)与文件所有者(如nobody)不一致。解决方案:
- 让主机商把项目目录所有者改为 PHP 运行用户
- 或给项目目录设置
775权限并确保两者在同一用户组- 或不折腾,直接手动覆盖升级
- 🎨 双图层动态背景:背景图自动切换,毛刺淡出 + 弹性淡入动画,支持一键启停轮播
- 🖼️ Banner 轮播:顶部横幅自动切换,左右滑动 + 光效扫过;切换前先探测图片可解码,失败时保留旧图不闪白,从"无图"恢复到"有图"走 0.7s 淡入
- 💾 本地图片缓存:背景 / Banner 使用 IndexedDB 缓存图片(Blob),回访秒显 + 自动切换一次到上次预取的图,断网也能轮播,缓存节奏跟随后台设置的轮播间隔
- 🔄 回访连贯性:
lastShownKey只在每次切换成功后写入(含轮播切换、回访秒显、回访 1.5s 后切下一张),因此回访时首屏显示的一定是"上次关闭页面时最后看到的那张图",即使是轮播过程中关闭的也一样,视觉完全连贯;回访续播时优先复用上次预取的图作为 1.5s 后的切换目标;API 优先和本地优先共用同一套续播骨架 - 🔍 内容指纹去重:API 池入池前计算图片内容指纹(头 32KB + 尾 32KB 双 FNV 采样),相同内容的图不会重复入池
- 🎯 待播槽预取:轮播切换前先在后台预取下一张,在线优先拉新图,失败才用缓存兜底,切换顺滑无等待;后台补池节流不阻塞预取,预取 Promise 快速 resolve;每次切换成功后立即触发下一次预取,待播槽永远有货,不会空转
- 🆘 本地背景兜底:外部 API 挂掉时可用本地图兜底,支持「API 优先 / 本地优先」自由切换;本地图按「桌面端 / 移动端 / 通用」自动分类,前台优先按屏幕方向取图,方向不匹配或加载失败会自动交叉兜底,彻底杜绝黑屏;Banner 兜底则优先使用桌面端横向图,不受手机方向影响;当 Banner 本地图全部被手动标成 mobile 时也会退回使用全部图,绝不因过滤后为空而失效。
- ✅ 解码验证:所有取图路径(API / 池 / 本地)在入池、入缓存、切换前都验证解码,失败的图不入队列、不出屏
- 👤 悬浮圆形头像:头像半悬浮在 Banner 下方,默认 10 秒内点击 10 次进入后台(次数与时间窗口可在后台配置)
- 📱 完整响应式:PC / 移动端自适应,移动端顶部固定、底部贴边
- 🔖 多分类标签页:自定义分类名称与图标,支持 emoji 或图片
- 🃏 卡片列表:支持链接卡片、图片卡片,图片卡片点击弹窗展示大图,弹窗大图支持长按保存
- 🎨 卡片悬停文字渐变:桌面端鼠标悬停时,卡片名与副标题柔和过渡到蓝色(深浅色模式各自适配)
- 🔖 分类菜单悬停光晕:未激活菜单悬停时浮起并亮起蓝色背景与光晕,与激活态视觉一致;激活态未悬停时不额外浮起,高度与未激活对齐
- 🎯 分类切换平滑过渡:切换分类时卡片列表高度平滑过渡,激活横条从旧按钮滑动到新按钮;电脑端先锁文档高度、再让滚动位置与高度/居中 margin 同相缓动,从"卡片多切到卡片少"时页面同步平滑上滚,无跳帧
- 🌓 主题系统:跟随系统 / 浅色 / 深色模式,前后台共用同一份主题偏好,配置保存在浏览器本地存储,内置防闪烁预解析。内置硬件性能自适应探测,自动识别 GPU 与设备性能,高配设备享受丝滑过渡,低端或纯 CPU 渲染设备自动降级保证流畅。
- 🔄 三态轮播开关:点击在
关闭 / 开启模式间切换;只要当前不处于持续新图状态,长按 3 秒(带蓄能环动画)即可进入持续新图模式(强制 API 优先,按钮变橙色)。再次点击或长按可退出。 - 👘 看板娘:可拖动、记忆位置、切换形象,支持多个 GIF / PNG / WebP / JPG。支持
<link rel="preload">提前拉取首图,并在加载页结束后延迟执行,防止气泡计时被吃掉;形象图加载失败会自动重试(最多 2 次),仍失败时降级为圆形 ✨ 悬浮按钮(点击直接打开控制面板)。 - 💬 看板娘语录:本地语录 + 远程 API 交替出场,时段问候 + 洗牌队列不重复
- ⚙️ 看板娘控制面板:点击语录气泡上的齿轮打开,或当语录关闭时点击看板娘打开
- ✨ JS 特效:可上传或引用远程 JS,前台控制面板按需开关。特效脚本在加载页结束后由
page-ready触发动态注入执行。可控特效(同时暴露start/stop)在面板里开关即时生效;固定特效(无成对start/stop)面板里同样可以开关,但每次开关会先写 cookie 再刷新页面。若特效脚本已加载但未注册 matchKey,开关会直接禁用并显示 ⚠ 图标,不再触发刷新死循环。 - 🎨 加载页主题:可上传多个 CSS 主题,前台底部独立按钮可让用户自选加载动画
- 📢 首页公告条:最新一条启用的公告会在首页顶部显示为紧凑横幅(类型徽章 + 标题 + 查看详情链接),点击「✕」可关闭,关闭后同一公告不再显示
- 📢 站内公告页:独立的
/announcement.php公告页面(列表风格:时间在左上、标题大字、正文紧凑),404 页「查看公告」跳转到该页,支持 4 种公告类型(通知 / 成功 / 警告 / 错误),后台「📢 公告管理」标签页可增删改查 - 🔗 可配置页脚:两行显示——第一行版权信息,第二行
Powered by 项目名 · GitHub 链接,全部后台可配,GitHub 链接有独立开关 - 🌐 全站统计代码:后台粘贴一次,前台 / 后台所有页面底部统一输出
- 📝 用户文案安全转义:卡片名称、副标题、分类名、看板娘名称、加载页名称/描述、UA 模板名等用户可编辑内容,含
<>等特殊字符时不会破坏页面结构;即使文案里填了</script>,也只会被当作普通字符展示 - 💬 文本溢出提示:卡片名称/副标题溢出时显示 Tooltip,移动端长按精准触发(自带 10px 滑动容错,手抖也不易取消)
- 🛑 长按防误触:优化了移动端长按页面空白区域时,系统选区手柄计算错位导致残影的 Bug,同时对纯视觉容器精准隔离选中,不影响正常长按复制文字。已精准拦截卡片、头像的系统长按菜单,但大图预览依然允许长按保存,体验媲美原生 App。
- 🧊 卡片按压反馈:卡片支持 3D 倾斜、按压缩放等交互效果
- ⚡ 首屏优化:配置提前到
<head>,首图预热请求先发,加载动画等到图片真正可解码才关闭;弱网熔断 + 柔和兜底底色,弱网/无网下绝不黑屏 - 🚪 未初始化自动引导:首次部署后访问前台会自动跳转到
/admin,弹出安装向导
- 🔐 安全登录:首次访问初始化管理员,密码哈希存储,
.admin.conf防直读;生产环境已关闭display_errors并开启log_errors,PHP 报错写入日志而不暴露给访问者 - 🧭 4 步安装向导:管理员账号 → 站点信息 → 行为配置 → 完成预览,首次安装约 2 分钟,所有字段留空则回退默认值
- 🔄 在线升级:点击后台顶部栏的「版本号(如 v1.1.0)」胶囊按钮,进入
admin/update.php独立升级页,对接 GitHub Releases API,5 分钟缓存,自动检查版本 → 环境检测 → 下载更新包(SHA256 校验 + 路径穿越防护)→ 应用升级(先备份再覆盖,失败自动回滚)→ 一键回滚。环境不支持时自动降级为「去 GitHub 下载」按钮 - 📋 卡片管理:新增、编辑、删除、拖拽排序;支持链接 / 图片两种类型
- 🗂 分类管理:新增、重命名、修改图标、删除;分类下有卡片时禁止删除;支持长按拖动排序
- 🖼 图片管理:上传、删除、重命名、排序、预览;重命名自动同步卡片图标 / 弹窗大图 / 分类图标,以及站点设置里的头像 / favicon / 后台头像(带前导
/与不带/两种形式都覆盖);删除采用两层确认:先弹常规确认(防误点)→ 后端检测引用 → 若图片正在被卡片、分类或站点设置使用,再弹第二个弹窗列出所有引用位置,确认后带force重试并同步清空对应引用(头像 / 网站图标回退到默认值),避免留下裂图 - 🖼️ 缩略图加速:图片管理 / 看板娘 / 本地背景的网格显示统一使用服务器生成的缩略图(长边 400px),点击「预览」才加载原图;网格里的
photo-cell加了content-visibility: auto,浏览器会跳过屏幕外元素的布局与绘制;切 tab 从「等几十张大图解码」变成「秒开」 - 🖼️ 本地背景分批挂载:进入「🖼 本地背景」标签页时,图片通过
requestAnimationFrame每帧挂载 4 张,避免一次性挂满导致主线程卡顿 - ✨ JS 特效管理:上传
.js、URL 添加远程 JS、启用 / 禁用、排序、编辑显示名称 / 图标;列表里以「可控 / 固定」徽章自动区分特效类型;matchKey 只在首次识别新特效时自动提取,手动修改后的值永久保留;编辑弹窗里提供「🔄 自动读取」按钮,可从脚本内容重新提取 matchKey;后台禁用固定特效时会弹出确认框 - 👘 看板娘管理:上传 GIF / PNG / WebP / JPG、启用 / 禁用、排序、重命名、删除
- 💬 语录设置:通用语录、五个时段的分时段语录、显示时长、远程 API 地址与字段名
- 🎨 加载页管理:上传
.css主题、启用 / 禁用、排序、编辑名称 / 描述 / 文件名;注入时统一转义特殊字符,.a > .b子选择器不会破坏页面结构 - 🖼 本地背景兜底:背景 / Banner 独立管理,支持上传、API 抓取、启用 / 禁用、预览 / 删除 / 重命名 / 拖动排序、优先级切换;「自动分类」按钮一键根据图片宽高重新纠正桌面端/移动端标签,并自动按「移动端(竖屏) / 桌面端(横屏) / 通用」分组展示,每组带标题与数量,视觉清晰;分类结果通过三种颜色的 toast 提示:全部成功 = 绿色,部分成功(有识别失败) = 黄色,全部失败 = 红色
- 🎭 UA 伪装管理:内置移动端 / 桌面端 UA 模板,可新增、编辑、删除自定义模板;背景与 Banner 抓取可分别选择 UA;这里的模板就是「立即抓取」实际使用的请求头——默认背景走移动端模板(抓竖图)、Banner 走桌面端模板(抓横图)
- 📢 公告管理:新增 / 编辑 / 删除 / 置顶 / 启用 / 禁用,4 种公告类型(通知 / 成功 / 警告 / 错误),公告内容支持 HTML 但不允许
<script> - 🌐 全站统计代码:独立标签页,粘贴一次全站生效,支持任意 HTML 片段
- ⚙ 站点设置:标题 / 副标题 / 头像 / favicon / API / 轮播间隔 / 缓存数量 / 连点次数 / 页脚版权 / GitHub 链接(含开关) / Powered by 项目名与链接等
- 🔢 数字步进器:配置类数字输入框自动带
− / +步进按钮,支持min / max / step(本地背景抓取数量等紧凑输入框保留原始外观,避免撑宽挤压按钮) - 🖱 拖拽排序修复:拖拽卡片 / 图片 / 分类后,页面其他按钮的点击不再被误吞(旧版会 400ms 内拦截所有点击),单击拖拽元素自身仍然被安全拦截
- 💾 备份恢复:导出完整
data.json,导入覆盖全部数据 - 🗑 清除数据:三种模式——"指定内容 / 清除全部 / 完全清空"
- 🔐 账号设置:修改管理员用户名、密码
- 🏷 版本号显示:后台顶部栏显示当前版本
v1.1.0
网站根目录
├── index.php # 前台首页,读取 admin/data.json 渲染
├── announcement.php # 站内公告页(公开访问)
├── analytics.php # 全站统计代码输出模块(前台/后台共用)
├── 404.php # 自定义 404 页面(「查看公告」跳转到 announcement.php)
├── admin/ # 后台管理目录
│ ├── index.php # 后台管理入口
│ ├── update.php # 在线升级页(独立页面,不嵌入标签栏)
│ ├── defaults.php # 默认值唯一权威来源(前后台共用 + 升级常量)
│ ├── data.json # 数据文件(首次安装生成)
│ ├── .admin.conf # 管理员账号配置(PHP 防直读,密码哈希)
│ ├── .htaccess # 保护 .admin.conf / data.json 不被直接下载
│ └── .update/ # 在线升级工作目录(自动创建)
│ ├── update.lock # 并发锁
│ ├── github_release_cache.json # GitHub Release 5 分钟缓存
│ ├── package.zip # 下载的更新包
│ ├── package/ # 解压后的更新包
│ ├── state.json # 升级状态机(tmp+rename 原子写入)
│ ├── history.json # 升级历史(最多保留 50 条)
│ └── backup/ # 代码备份(按时间戳分目录,保留最近 3 份)
├── assets/ # 静态资源
│ ├── style.css # 前台样式
│ ├── admin.css # 后台样式(含第 27 章在线升级页样式)
│ ├── frontend.js # 前台脚本
│ ├── admin.js # 后台脚本
│ ├── mascot.js # 看板娘 + 特效控制器
│ ├── js/ # JS 特效文件(自动创建)
│ └── preloaders/ # 加载页 CSS 主题(自动创建)
│ ├── sakura-spin.css # 内置主题:樱花旋转
│ ├── elegant.css # 内置主题:浮光
│ ├── apple.css # 内置主题:Apple
│ ├── tech.css # 内置主题:脉冲
│ ├── cyberpunk.css # 内置主题:赛博朋克
│ ├── ink.css # 内置主题:水墨
│ ├── minimal.css # 内置主题:极简
│ ├── nature.css # 内置主题:自然
│ ├── neon.css # 内置主题:霓虹
│ ├── sakura.css # 内置主题:樱花
│ ├── starry.css # 内置主题:星空
│ └── vaporwave.css # 内置主题:蒸汽波
├── photos/ # 图片存储目录(需写权限)
│ ├── .htaccess # 禁止在此目录执行脚本
│ ├── QQ.jpg # 默认头像
│ ├── zy.png # 默认分类图标「导航」
│ ├── lx.png # 默认分类图标「联系」
│ ├── thumbs/ # 图片管理缩略图(自动创建)
│ ├── kanban/ # 看板娘图片目录(自动创建)
│ │ ├── 妹妹.gif
│ │ ├── 姐姐.gif
│ │ ├── 喵小娘.gif
│ │ ├── 月薪喵.gif
│ │ ├── 七濑胡桃.gif
│ │ ├── 舞娘娘.gif
│ │ ├── 柠檬妹.gif
│ │ └── 熊宝宝.gif
│ ├── kanban_thumbs/ # 看板娘缩略图(自动创建)
│ ├── localbg/ # 背景本地兜底图目录(自动创建)
│ ├── localbg_thumbs/ # 背景兜底缩略图(自动创建)
│ ├── localbanner/ # Banner 本地兜底图目录(自动创建)
│ └── localbanner_thumbs/ # Banner 兜底缩略图(自动创建)
├── .gitignore # 排除运行时数据
├── CHANGELOG.md # 版本变更记录
├── LICENSE # MIT 协议
└── README.md
后台会自动创建
photos/、photos/thumbs/、photos/kanban/、photos/kanban_thumbs/、photos/localbg/、photos/localbg_thumbs/、photos/localbanner/、photos/localbanner_thumbs/、assets/js/、assets/preloaders/、admin/.update/目录(如果不存在)。
admin/defaults.php是所有默认值(site_defaults()/tips_defaults()/factory_defaults()/ua_builtin_templates())和升级常量(UPDATE_GITHUB_REPO/UPDATE_DIR/UPDATE_MAX_PACKAGE_MB/UPDATE_MAX_EXTRACTED_MB/UPDATE_BACKUP_KEEP)的唯一权威来源,前后台共用,避免重复定义和配置漂移。
admin/data.json与admin/.admin.conf会在首次安装 / 保存时生成。
assets/preloaders/中实际存在的.css会被后台自动扫描,内置主题数量以目录为准。
photos/localbg/与photos/localbanner/是平铺目录:桌面端 / 移动端 / 通用的图混合存放在同一目录,靠data.json里的target字段区分(前台按方向过滤,后台可直接一键分类并按方向分组展示)。
admin/.update/是在线升级工作目录,只由admin/update.php与admin/index.php的update_*API 读写,绝不允许被升级包覆盖。
- PHP 7.3+(推荐 PHP 7.4+,后台使用
array_key_first()) - 开启 PHP Session
- 支持 JSON 扩展
- 浏览器支持 IndexedDB(现代浏览器均支持)
- 抓取本地背景需要:允许
allow_url_fopen,或服务器能访问外网 - 在线升级需要:PHP
zip扩展(ZipArchive),服务器能访问api.github.com和github.com - PHP 图片扩展(至少其一,用于生成缩略图):
- Imagick(推荐):处理大图用磁盘缓存,几乎不吃 PHP 内存
- GD:PHP 默认扩展,大部分主机自带;内存受限时单张处理 4000×3000 以内较稳
- 两者都没有也不影响使用,只是不生成缩略图(自动退回原图显示),其它功能完全正常
- 目录写权限:
admin/:写入data.json和.admin.confadmin/.update/:在线升级工作目录(自动创建)photos/:上传图片photos/thumbs/:图片管理缩略图(自动创建)photos/kanban/:上传看板娘,自动创建photos/kanban_thumbs/:看板娘缩略图(自动创建)photos/localbg/、photos/localbg_thumbs/:背景本地兜底,自动创建photos/localbanner/、photos/localbanner_thumbs/:Banner 本地兜底,自动创建assets/js/:上传 JS 特效,自动创建assets/preloaders/:上传加载页 CSS,自动创建
- PHP 上传限制建议不低于:
- 图片:5MB
- JS:2MB
- 看板娘:10MB
- 加载页 CSS:2MB
- PHP 内存限制:后台入口会自动
@ini_set('memory_limit', '512M');如果主机禁止覆盖(disable_functions等),建议手动在 PHP 配置里调到 512M 以上(生成缩略图需要);使用 Imagick 时 PHP 内存基本不是瓶颈
-
上传文件
将全部项目文件上传至 PHP 网站根目录。 -
设置权限
chmod -R 755 admin photos assets/js assets/preloaders chmod -R 755 photos/thumbs photos/kanban_thumbs chmod -R 755 photos/localbg photos/localbg_thumbs chmod -R 755 photos/localbanner photos/localbanner_thumbs
-
访问后台初始化
- 未初始化时,直接访问前台
/index.php会自动跳转到/admin,无需手动输入后台地址 - 或直接访问
/admin(或/admin/index.php) - 首次访问会弹出 4 步安装向导:
- 管理员用户名与密码(至少 6 位)
- 站点信息(标题、副标题、头像、favicon、API、页脚版权 / GitHub / Powered by)
- 行为配置(轮播间隔、缓存数量、连点次数 / 时间窗口)
- 完成预览
- 提交后自动登录进入后台
- 未初始化时,直接访问前台
-
配置站点
登录后台 →「站点设置」中修改标题、头像、API、轮播间隔等;需要统计代码时到「🌐 统计代码」标签页粘贴;想要 API 挂掉时兜底就到「🖼 本地背景」标签页配置;需要伪装抓取 UA 就到「🎭 UA 伪装」标签页配置。 -
首次进后台会生成缩略图
首次登录后会为已有的所有图片生成缩略图,每个请求最多处理 30 张,防止 PHP 内存爆掉。图片多的话可以多刷新几次后台,缩略图会逐渐补齐。缩略图生成期间不影响后台的其它操作。 -
想在线升级的话
确认服务器装了 PHPzip扩展、能访问api.github.com;点击后台顶部栏的版本号,环境检测通过后就能在线升级。环境不通过时点「🔗 去 GitHub 下载」手动覆盖即可。
.htaccess 在 Nginx 下不生效,需要在站点配置里手动加以下规则,否则 .admin.conf / data.json 可能被直接下载,photos/ 里的脚本可能被执行:
# 保护后台敏感文件
location ~ ^/admin/.*\.(conf|json|bak|tmp|log|md)$ {
deny all;
return 404;
}
# 保护在线升级工作目录
location ~ ^/admin/\.update/ {
deny all;
return 404;
}
# 禁止图片目录执行脚本
location ~ ^/photos/.*\.(php|phtml|php3|php4|php5|php7|php8|phar|pl|py|jsp|asp|aspx|sh|cgi)$ {
deny all;
return 404;
}改完后 nginx -t && nginx -s reload 生效。
验证是否生效:浏览器访问 https://你的域名/admin/data.json,应返回 403 或 404。
大多数共享主机支持 Apache,.htaccess 自动生效。共享主机用户在线升级大概率会因权限不一致失败(PHP 运行用户 ≠ 文件所有者),环境检测不通过时会自动降级为「去 GitHub 下载」,请按 README 手动覆盖升级。
- 未初始化时,前台任意页面会自动跳转到
/admin进行安装 - 初始化完成后:前台首页在配置的时间窗口内连续点击头像达到配置次数,自动跳转
/admin/index.php(默认 10 次 / 10 秒) - 或直接访问
/admin
点击后台顶部栏的版本号胶囊进入 admin/update.php:
- 自动检查:打开页面即自动拉取 GitHub Releases 最新版本,显示当前版本 / 最新版本 / 是否有更新
- 环境检测:显示 PHP 运行用户、文件所有者、是否可写
- ✅ 可写 → 显示「⬆ 立即升级」按钮
- ❌ 不可写 → 显示「🔗 去 GitHub 下载」按钮,并展示具体原因
- 一键升级:点「立即升级」→ 自动下载 → SHA256 校验 → 解压 → 备份 → 覆盖 → 完成
- 一键回滚:升级完成后,如果新版本有问题,点「↩ 回滚到上一版本」即可
注意:
- 在线升级只换代码文件,
data.json、photos/、assets/js/、assets/preloaders/永远不动 - 升级前会备份当前代码到
admin/.update/backup/<时间戳>/,默认保留最近 3 份 - 如果升级过程中遇到网络问题导致卡住,10 分钟后状态会自动转
failed,重新进升级页即可继续 - 升级成功后记得 Ctrl+F5 强制刷新 后台,让浏览器加载新版本的
admin.js/admin.css
- 管理员账号:用户名 + 密码(至少 6 位)+ 确认密码
- 站点信息:浏览器标签标题、主标题、副标题、头像、favicon、背景 API、Banner API、页脚版权、GitHub 链接 + 开关、Powered by 项目名 + 链接
- 行为配置:背景轮播间隔、Banner 轮播间隔、缓存图片数量、头像连点次数、点击时间窗口
- 完成预览:显示所有已填信息 + 「已预置 2 个默认分类(导航 / 联系)与 2 个加载页主题(优雅 / 苹果)」
留空策略:任何字段留空 → 回退到
admin/defaults.php的默认值。API 字段留空也一样会回退到 loliapi。
- 单指点击:在
关闭(播放图标)和开启(暂停图标)之间切换;在持续新图(橙色闪电)状态下点击则回到关闭。 - 长按 3 秒:只要当前不处于
持续新图状态,长按 3 秒都会触发蓄能光环动画,松开后进入 「持续新图」 模式(橙色闪电图标)。 - 持续新图模式:强制忽略后台的「本地优先」设置,每一轮轮播都强行请求新的 API 图片。
- 新增 / 编辑卡片:选择类型(链接 / 图片)、分类、名称、副标题、图标、跳转网址或弹窗大图
- 拖拽排序:按住行首
⋮⋮手柄 350ms 后拖动,拖动后行内出现「保存排序 / 取消」按钮;顺序未变时不显示按钮 - 删除卡片:点击行尾 🗑 按钮
- 添加分类:点击「+ 添加分类」
- 编辑分类:点击分类标签上的 ✎,可修改名称和图标(emoji 或图片路径)
- 删除分类:点击 ✕,若分类下还有卡片则无法删除
- 拖拽排序:长按分类标签 350ms 后拖动,松手自动保存
- 上传:点击虚线框或拖拽图片到区域,支持 jpg / jpeg / png / gif / webp,单张 ≤ 5MB
- 也可直接将图片放入
photos/目录,刷新后台自动出现 - 排序:长按
⋮⋮拖动,松手自动保存 - 重命名:悬停图片点击 ✎,扩展名不变,自动更新卡片 / 分类中的引用以及站点设置里的头像 / favicon / 后台头像
- 删除:悬停图片点击 ✕
- 第一层确认:任何删除都会先弹「确定删除图片「xxx」吗?」(防误点)
- 点确定后,后端检测引用情况:
- 若图片未被引用 → 直接删除,toast 提示"图片已删除"
- 若图片正在被使用(卡片图标 / 卡片弹窗大图 / 分类图标 / 前台头像 / 网站图标 / 后台头像)→ 第二层确认弹窗列出所有引用位置,再点确定才会真正删除,同时清空对应引用(头像 / 网站图标回退到
site_defaults()里的默认值),避免留下裂图
🖼 列表里显示的是缩略图(长边 400px),点击「预览」才加载原图。
缩略图在上传时自动生成,也可以把原图丢进photos/后刷新后台自动补生成。
删除 / 重命名原图时,缩略图会一并处理。
- 上传
.js文件:点击「+ 上传特效」,文件 ≤ 2MB - URL 添加:点击「🔗 URL 添加」,输入远程 JS 地址
- 启用 / 禁用:开关控制
- 排序:长按
⋮⋮拖动,松手自动保存 - 编辑:点击 ✎ 修改前台显示名称、图标,以及 matchKey
- 前台控制面板:看板娘面板或右下角 ✨ 按钮中可开关特效
- 也可直接将
.js放入assets/js/,刷新后台自动识别
可控特效 vs 固定特效(后台自动识别)
admin/index.php同步 JS 文件时会检测模块是否同时暴露start/stop,并写入jsEffects[].autoToggle:
- 可控特效:模块同时有
start和stop,后台列表显示「可控」徽章。前台面板中开关即时生效(直接调用start()/stop(),不刷新页面)。- 固定特效:没有成对的
start/stop(或只写了Effect.start()),后台列表显示「固定」徽章。前台面板里同样可以开关,但因为缺少"原地停止"的能力,每次开关会走刷新路径 —— 先把matchKey写进localStorage.effects_states和nuomi_effects_offcookie,再刷新页面;刷新后index.php根据 cookie 决定是否把该脚本<script>注入。
后台禁用的语义差异
- 对可控特效:后台禁用后,该特效从「默认启用」列表里消失;但前台面板里仍保留开关(
showInPanel = cfgEnabled || runtimeToggle),访客自己可以打开。- 对固定特效:后台禁用时会弹出确认框,提示「该特效不支持自启停,禁用后将从首页及控制面板中移除,访客无法手动启用」。确认后该特效会从前台面板里完全消失,需要后台重新启用才能恢复。
matchKey 的语义
- 后台只在首次识别新特效时自动从脚本内容提取 matchKey;已经存在的值(无论自动提取的还是手动修改的)永久保留,刷新后台不会再覆盖。
- 编辑弹窗里有一个「🔄 自动读取」按钮,可以随时从当前 JS 文件内容重新提取一次,确认后手动保存。
- 如果脚本已经加载到前台、但
EffectsRegistry里注册的name与data.json里的matchKey对不上,前台面板里点开关会直接禁用开关并显示 ⚠ 图标(不会再触发刷新死循环)。此时去后台点「🔄 自动读取」或手动修正 matchKey,保存后刷新前台即可正常使用。
特效 JS 需注册到
window.EffectsRegistry:window.EffectsRegistry = window.EffectsRegistry || []; window.EffectsRegistry.push({ name: 'snowfall', module: Snowfall });
- 上传:点击虚线框或拖拽 GIF / PNG / WebP / JPG,单个 ≤ 10MB
- 也可直接放入
photos/kanban/,刷新后台自动出现(内置妹妹.gif/姐姐.gif/喵小娘.gif/月薪喵.gif/七濑胡桃.gif/舞娘娘.gif/柠檬妹.gif/熊宝宝.gif可作为默认形象) - 启用 / 禁用:右上角开关
- 排序:长按
⋮⋮拖动,松手自动保存 - 重命名:悬停点击 ✎,磁盘文件与显示名称同步修改
🖼 GIF 会取第一帧作为缩略图,所以网格里不会再播动态图,切 tab 更顺。
点击「预览」时加载的仍然是原 GIF(动图正常播放)。
进入后台「看板娘」标签页,页面底部有「💬 语录设置」区块,可配置:
- 默认开启随机语录:关闭后,用户仍可在看板娘面板里手动打开
- 语录显示时长:秒,范围 0.5 ~ 60,默认 3 秒(存储为毫秒)
- 通用语录:每行一条,随时段随机抽取(与分时段语录一起洗牌)
- 分时段语录:五个时段各一组,按当前时间自动匹配
- 🌙 深夜(0:00 - 5:00)
- ☀️ 早上(5:00 - 11:00)
- 🍚 中午(11:00 - 13:00)
- ☕ 下午(13:00 - 17:00)
- 🌆 晚上(17:00 - 23:00)
- 远程语录 API 地址:可选,如
https://v1.hitokoto.cn/?c=b - API 返回的字段名:如
hitokoto(默认值text)
进入后台「加载页」标签页,可管理首屏加载动画主题:
- 总开关:控制前台是否启用首屏加载遮罩
- 上传:点击虚线框或拖拽
.css文件,单个 ≤ 2MB,可批量 - 自动识别:也可直接将
.css放入assets/preloaders/,刷新后台自动出现 - 启用 / 禁用:每个主题独立开关
- 排序:长按
⋮⋮拖动,松手自动保存,排在首位的主题自动成为默认主题 - 编辑:点击 ✎ 修改显示名称、描述、文件名(磁盘文件同步重命名)
- CSS 文件规范:
- 文件顶部可写注释定义名称和描述:
/* @name 樱花旋转 */ /* @description 樱花旋转 + 呼吸缩放 + 花瓣错拍闪烁 */
- 不要包含
<style>、<script>、</script>、<?php等标签;上传与后台扫描时会拒绝<?php、<?=、</style>、<script、</script - CSS 里正常的
.a > .b子选择器不会破坏页面结构,注入到<script>时会自动转义
- 文件顶部可写注释定义名称和描述:
进入后台「🖼 本地背景」标签页,为背景 / Banner 配置 API 挂掉时的兜底图。
核心特点:
- 完全独立:背景与 Banner 分开存储(
photos/localbg/与photos/localbanner/),分开开关,分开优先级 - 双录入方式:
- 手动上传:点击虚线框或拖拽图片,支持 jpg / png / gif / webp,单张 ≤ 5MB
- 从 API 抓取:填张数(1 ~ 20),点「立即抓取」→ 服务器从当前配置的 API 拉图并存到本地
- 目标设备自动识别(desktop / mobile / both):
- 上传或抓取时,系统会读取图片宽高自动打标:宽 > 高 × 1.1 →
desktop;高 > 宽 × 1.1 →mobile;其余 →both - 后台面板顶部的 「自动分类」 按钮,可一键按宽高重算所有图片的
target字段,纠正历史打错的标签 - 自动分类后网格会按设备方向分组展示:依次显示「📱 移动端(竖屏) · N 张」「🖥 桌面端(横屏) · N 张」「🌐 通用 · N 张」三个分组,每组带分隔标题和数量,方便一眼看清各方向的图片数量;分组内仍然支持拖拽排序、编辑、预览、删除
- 自动分类的结果提示按成功程度区分颜色:
- ✅ 全部成功:绿色 toast
⚠️ 部分成功(有图片读取失败):黄色 toast- ❌ 全部失败(没有一张能识别):红色 toast
- 单张图片点 ✎ 编辑时,可在弹窗里手动指定:✨ 自动识别 / 🖥 桌面端 / 📱 移动端 / 🌐 通用
- 前台按屏幕方向(
innerWidth >= innerHeight→ desktop)自动过滤:target === 'both'或target === 当前方向的图优先参与轮播 - 交叉兜底防黑屏:如果当前方向没有图,或者某张图加载失败,前台会自动拿相反方向的图顶替,保证绝不黑屏
- Banner 优先横屏:Banner 区域本身是横向轮播,前台优先只取
desktop和both的横图。如果所有 Banner 本地图都被手动改成了mobile,前台会退回使用全部图,不会因为过滤后为空导致 Banner 本地兜底彻底失效。
- 上传或抓取时,系统会读取图片宽高自动打标:宽 > 高 × 1.1 →
- 抓取默认 UA:点「立即抓取」时,服务器会读
site.localBgUaId/site.localBannerUaId里保存的模板 ID 拼请求头。默认背景走移动端模板(抓竖图)、Banner 走桌面端模板(抓横图)——想换方向去「🎭 UA 伪装」页切换对应模板即可。 - 内容去重:上传与抓取都会按内容 MD5 去重,同一张图不会存两份
- 独立缓存:本地图会写入浏览器 IndexedDB(键前缀
bg_local:/banner_local:),回访秒显、零流量 - 隔离存储:与 API 池的缓存键完全隔离,不参与 FIFO 淘汰,不挤占池空间
- 优先级可调(每类独立):
- API 优先(默认):API(重试 3 次)→ 池里历史图 → 本地图 → 空白
- 本地优先:本地图按顺序循环;如果本地图数量少于缓存池上限,系统会自动在后台用 API 图填满剩余坑位
- 本地优先队列:本地优先模式下,本地图与 API 池会按顺序循环取图,游标保存在
localStorage.nuomi_queue_cursor_<type> - 缩略图加速:网格显示缩略图(长边 400px,自动生成到
photos/localbg_thumbs//photos/localbanner_thumbs/),点击「预览」才加载原图 - 图片分批挂载:进入「🖼 本地背景」标签页后,图片用
requestAnimationFrame每帧挂 4 张 - 操作完整:上传 / 预览 / 删除 / 重命名(磁盘同步,弹窗内可选自动识别 / 桌面端 / 移动端 / 通用) / 拖动排序
目录结构:
photos/
├── localbg/ # 背景本地兜底图(desktop / mobile / both 平铺混放)
├── localbg_thumbs/ # 背景兜底缩略图(自动生成)
├── localbanner/ # Banner 本地兜底图(建议放横图)
└── localbanner_thumbs/ # Banner 兜底缩略图(自动生成)
也可以直接把图片丢进这两个目录,刷新后台自动识别,缩略图会自动补生成。
手动放进去的图会按宽高自动识别 target,想要both就放接近正方形的图,或者进后台点「自动分类」重新算。
推荐用法:
| 你的场景 | 建议操作 |
|---|---|
| 只给手机用 | 默认背景走 ua_mobile_default 直接点「立即抓取」,抓一批竖图 |
| 只给 PC 用 | 默认 Banner 走 ua_desktop_default 抓一批横图;背景想给 PC 用,去「🎭 UA 伪装」页把背景模板切到 ua_desktop_default 再抓 |
| PC + 手机都要 | 背景默认移动端抓一批竖图;Banner 默认桌面端抓一批横图;也可手动上传对应方向的图 |
| 不区分方向 | 手动上传接近正方形的图,会自动打 both |
| 想纠正打错的标签 | 点「自动分类」按宽高重算,网格会立刻按新方向分组展示 |
进入后台「🎭 UA 伪装」标签页,可维护抓取本地背景时携带的请求头。
核心特点:
- 内置模板:
ua_mobile_default:iOS 18 Safari 移动端ua_desktop_default:Windows 11 Chrome 桌面端
- 可编辑不可删除:内置模板可以改内容,但不能删除。
- 自定义模板:可新增、编辑、删除自定义 UA 模板。
- 独立字段:
site.localBgUaId/site.localBannerUaId各存一个模板 ID,这两个字段就是「立即抓取」实际使用的请求头。 - 请求头字段:
User-AgentAcceptAccept-LanguageAccept-Encoding- 额外请求头,每行一个,格式
Name: value
- 自动回退:如果删除正在使用的自定义模板,会自动回退到对应默认模板。
- 默认值语义:
localBgUaId默认是ua_mobile_default→ 背景抓取默认走移动端 UA,抓竖图localBannerUaId默认是ua_desktop_default→ Banner 抓取默认走桌面端 UA,抓横图
进入后台「📢 公告管理」标签页:
- 新增公告:点击右上角「+ 新增公告」
- 公告字段:
- 标题(必填,最多 100 字)
- 类型:ℹ️ 通知 / ✅ 成功 /
⚠️ 警告 / 🚫 错误,分别对应蓝 / 绿 / 黄 / 红四种配色 - 内容:支持 HTML 标签(
<p>/<strong>/<a>/<ul>/<li>/<code>/<img>等),不允许<script>标签 - 置顶:置顶的公告会排在最上方
- 启用:禁用的公告不会显示在公告页
- 列表操作:启用 / 禁用开关、📌 置顶按钮、✎ 编辑、🗑 删除
- 公告页:
https://你的域名/announcement.php,自动按「置顶优先 + 创建时间倒序」排序,主题跟随localStorage.themeMode - 404 页「查看公告」按钮:跳转到
announcement.php
进入后台「站点设置 → 前台 → 基础设置」,页脚相关字段:
- 页脚版权信息(
copyright):显示在页脚第一行,留空则不显示 - 页脚 GitHub 链接(
githubUrl):显示在页脚第二行,留空则不显示 - 显示 GitHub 链接(
githubEnabled):独立开关,关闭后保留地址但不显示 - 页脚 Powered by 项目名(
poweredBy):显示在页脚第二行,前面自动加「Powered by 」 - 页脚 Powered by 链接(
poweredByUrl):留空则 Powered by 只显示为纯文本
默认值(可在 admin/defaults.php 里修改):
copyright:© 2026 糯米の引导页githubUrl:https://github.com/nmrt0316/MochiNav(占位符,请替换为真实仓库地址)githubEnabled:1(开启)poweredBy:糯米の引导页poweredByUrl:同上占位符
进入后台「🌐 统计代码」标签页,可粘贴全站统计代码:
- 作用范围:前台、后台所有页面的
</body>前统一原样输出 - 支持内容:百度统计、Google Analytics、Google Tag Manager、Cloudflare Web Analytics、Umami、自建统计等任意 HTML 片段
- 格式校验:必须包含 HTML 标签,否则保存会被拒绝
- PHP 拦截:内容包含
<?php或<?=会被拒绝 - 留空即关闭:留空保存则不输出任何统计代码
- CDN 重复注入提醒:如果统计服务商已由 Cloudflare 等 CDN 在边缘节点自动注入,不要再往这里粘贴同一份代码
- 前台:
- 网站图标 favicon、浏览器标签标题、主标题、副标题、头像
- 图片路径支持两种形式:
photos/QQ.jpg(推荐,子目录部署也能正确解析)或/photos/QQ.jpg(根目录部署可用) - 背景轮播 API、Banner 轮播 API
- 背景轮播间隔(秒,3~60,默认 10)
- Banner 轮播间隔(秒,3~60,默认 6)
- 缓存图片数量(每池上限,2~20,默认 8)
- 进入后台的连点次数与时间窗口
- 首屏加载动画开关
- 页脚版权信息、GitHub 链接(含开关)、Powered by 项目名与链接
- 后台:登录页头像、登录页标题、登录页副标题、后台浏览器标签标题、后台顶部栏文字
⚠️ 站点设置里的所有改动保存后需要刷新前台页面才生效。
- 导出:下载完整
data.json备份(含卡片、分类、特效、看板娘、语录、站点设置、统计代码、公告) - 导入:上传备份 JSON,完整覆盖当前全部数据
- 指定内容:可多选清除「卡片与分类 / 图片 / JS 特效 / 看板娘 / 语录设置 / 站点设置 / 加载页主题 / 本地背景兜底 / UA 模板 / 公告」,确认词
RESET - 清除全部:恢复出厂设置,保留
photos/图片与管理员账号,确认词RESET - 完全清空:清空全部配置、所有图片目录、管理员账号,确认词
CLEAR,操作后需要重新初始化
「指定内容」勾选「UA 模板」会清除所有自定义模板,仅保留两个内置模板;勾选「公告」会清空所有公告;「清除全部」/「完全清空」会重置或清空 UA 配置与公告。
清除图片 / 看板娘 / 本地背景时,对应的缩略图目录也会一并清空。
清除数据不会清除admin/.update/在线升级工作目录(升级缓存、备份、状态都保留)。
- 修改用户名:需验证当前密码
- 修改密码:需验证当前密码,新密码至少 6 位
admin/data.json 中的 site 对象保存了所有站点配置。
| 字段 | 说明 | 默认值 |
|---|---|---|
pageTitle |
前台浏览器标签标题 | 糯米の引导页 |
title |
前台主标题 | 糯米の引导页 |
subtitle |
前台副标题 | 又一个引导页 |
avatar |
前台头像 | QQ 头像 URL |
favicon |
网站图标 | photos/QQ.jpg |
bgApi |
背景轮播 API | https://www.loliapi.com/acg/ |
bannerApi |
Banner 轮播 API | https://www.loliapi.com/acg/pc/ |
bgInterval |
背景轮播间隔(秒,3 ~ 60) | 10 |
bannerInterval |
Banner 轮播间隔(秒,3 ~ 60) | 6 |
cacheLimit |
每个缓存池上限张数(2 ~ 20) | 8 |
adminAvatar |
后台登录页头像 | photos/QQ.jpg |
adminTitle |
后台登录页标题 | 后台登录 |
adminSubtitle |
后台登录页副标题 | 糯米の引导页管理 |
adminPageTitle |
后台浏览器标签标题 | 糯米の引导页 · 后台管理 |
adminHeaderTitle |
后台顶部栏文字 | 管理后台 |
adminClickCount |
头像连点进入后台所需次数(3 ~ 30) | 10 |
adminClickWindow |
头像连点时间窗口(秒,1 ~ 60) | 10 |
preloader |
前台首屏加载遮罩开关 | 1 |
preloaderStyle |
默认加载页主题 key | elegant |
analyticsCode |
全站统计代码 | "" |
localBgPriority |
背景兜底优先级(api / local) |
api |
localBannerPriority |
Banner 兜底优先级(api / local) |
api |
localBgUaId |
背景抓取使用的 UA 模板 ID | ua_mobile_default |
localBannerUaId |
Banner 抓取使用的 UA 模板 ID | ua_desktop_default |
copyright |
页脚版权信息 | © 2026 糯米の引导页 |
githubUrl |
页脚 GitHub 链接 | https://github.com/nmrt0316 |
githubEnabled |
GitHub 链接开关(1 / 0) |
1 |
poweredBy |
页脚 Powered by 项目名 | 糯米の引导页 |
poweredByUrl |
页脚 Powered by 链接 | 同上 |
默认路径约定:
favicon/adminAvatar默认值是不带前导/的相对路径(photos/QQ.jpg),根目录和子目录部署都能正确解析。老版本数据里如果存的是/photos/QQ.jpg也照常使用,不受影响。
默认 UA 约定:
localBgUaId默认ua_mobile_default(背景抓取抓竖图)、localBannerUaId默认ua_desktop_default(Banner 抓取抓横图)。想换方向,去「🎭 UA 伪装」页切换即可。
数据文件顶层还包含:
| 字段 | 说明 |
|---|---|
localBg |
{ enabled: bool, images: [{file, name, fp, target}] } |
localBanner |
{ enabled: bool, images: [{file, name, fp, target}] } |
uaTemplates |
UA 伪装模板数组,每项含 id / name / ua / accept / acceptLanguage / acceptEncoding / extraHeaders / builtin |
announcements |
公告数组,每项含 id / title / content / type / pinned / enabled / created / updated |
localBg.images[] / localBanner.images[] 中每一项的字段:
| 字段 | 说明 |
|---|---|
file |
网站根目录相对路径,如 photos/localbg/xxx.jpg |
name |
显示名称(可重命名) |
fp |
内容 MD5 指纹(用于去重) |
target |
目标设备:desktop / mobile / both(缺省视为 both) |
关于缩略图路径:
thumb字段是运行时计算的派生字段,不会写入data.json。
统计代码独立于站点设置的其它字段,由公共模块 analytics.php 统一负责输出。
前台 index.php、admin/index.php、announcement.php、404.php 的 </body> 前都有一行:
<?php include __DIR__ . '/analytics.php'; ?>后台使用:
<?php include __DIR__ . '/../analytics.php'; ?>analytics.php 读取 admin/data.json 里的 site.analyticsCode,非空则原样 echo。
很多 CDN(例如 Cloudflare 的 Web Analytics)会在边缘节点自动往 HTML 的 </body> 前注入自己的探针脚本。这跟你在后台手填的代码是两套独立通道:
- 你手填的:由
analytics.php输出 - CDN 注入的:由 CDN 边缘节点插入
已经在用 CDN 自动注入的:不要往后台统计框再填一遍。想在后台统一管理:到 CDN 后台关掉自动注入,改为在后台手填。
前台背景与 Banner 图片使用 IndexedDB(数据库名 nuomi_img_cache,对象仓库 images)缓存图片 Blob,带来三个好处:
- 回访秒显:刷新页面立即显示上次缓存的图
- 断网兜底:API 挂掉 / 网络不通时,仍能显示缓存图
- 离线轮播:开轮播时若拉不到新图,可从缓存池里按顺序取图切换
| 项 | 说明 |
|---|---|
| 池子数量 | 背景、Banner 各一个独立池子 |
| 上限 | 由 cacheLimit 控制(默认 8 张,可后台改 2~20) |
| 淘汰策略 | FIFO(新进旧出,超上限删最旧的) |
| 内容去重 | 入池前按内容指纹比较,相同 fp 的图不会重复入池 |
| 未开轮播(池未满) | 每次回访拉 1 张新图入池,池慢慢变厚 |
| 未开轮播(池已满) | 不拉新图,从池里按顺序取一张填槽 |
| 开轮播(池未满或已满) | 每次切换拉 1 张新图;池满则 FIFO 覆盖最旧一张 |
| 池取图顺序 | 按时间升序顺序循环(A→B→C→…→A),一圈内不重复 |
nuomi_img_cache / images 对象仓库中,三类 key 严格隔离:
| 类型 | Key 格式 | FIFO 淘汰 | 参与顺序游标 |
|---|---|---|---|
| API 池 | bg:<时间戳>:<rand>:<fp> / banner:<时间戳>:<rand>:<fp> |
✅ 是 | ✅ 是 |
| 本地图缓存 | bg_local:<filepath> / banner_local:<filepath> |
❌ 否 | ❌ 否 |
| 坏图黑名单 | localStorage.nuomi_bad_local_<type>_<signature> |
❌ 否 | ❌ 否 |
隔离依赖字典序:':' (0x3A) < '_' (0x5F),所以 bg_local:... 永远不在 ['bg:', 'bg:\uffff'] 扫描范围内。
⚠️ 将来重构时不要把本地 key 前缀写成bg:开头(例如bg:local:xxx),否则会污染池的 FIFO 计数。
智能黑名单:弱网环境(网络超时)不会将图片加入黑名单,只有 404 / 解码失败等实质性损坏才会拉黑。
API 池入池前会计算图片内容指纹 fp:
- 取 Blob 头 32KB 与 尾 32KB,分别做 FNV 哈希
- 组合成
fp = <size(36进制)>_<headHash(36进制)>_<tailHash(36进制)> - 写入 key 末尾:
bg:<时间戳>:<rand>:<fp> - 入池前扫描池内已有 key,若存在相同
fp则丢弃新图,不入池
轮播不是"到点才去拉图",而是维护一个只存 1 张图的「待播槽」,切换时直接取槽,零等待。
关键:lastShownKey 只在切换成功后写入
lastShownKey 记录的是"上次关闭页面时最后实际显示在屏幕上的那张图",不是"上次预取的那张",也不是"回访刚开始秒显的那张"。它只在这三种时机更新:
- 轮播切换成功后(
doActualApply的usePick里,doSwitch返回ok后) - 回访首屏秒显成功后(
init里doSwitch(r.pick, true)返回ok后) - 回访 1.5s 后切待播槽成功后(
doSwitchAfterDelay里doSwitch(p, false)返回ok后)
未开轮播时的行为(每次回访都会跑,API 优先 / 本地优先共用同一套续播骨架):
回访:
⓪ <head> 预热前判断 localStorage.nuomi_pool_full_<type>
—— '1'(池满)或本地优先 → 不发预热请求
① showKey = lastShownKey(上次关闭页面时最后实际显示的那张)→ 秒显(无动画)
② 秒显成功后,setLastShownKey(showKey)(保持幂等,避免后续刷新丢状态)
③ 确定待播槽的图(nextReady):
- 若 lastPrefetchKey(或 localPrefetchId)仍在池中且不同于 showKey
→ 直接复用,不重复拉取
- 否则调 prefetchNext():从缓存进待播队列的池子里取一张放进待播槽
④ 等 preloader 消失(body.page-ready)
⑤ 再等 1.5s(让用户看清当前背景)
⑥ 平滑切换到待播槽里的那张(带动画)
⑦ 切换成功后,setLastShownKey(刚切到的那张)
⑧ 立即 prefetchNext():再取一张放进待播槽
⑨ 停住,等用户下次回访或开轮播
开轮播时的行为:
- 每次切换完 →
setLastShownKey(刚切到的那张)→ 立刻拉 1 张新图替换槽 - 拉到新图 → 切换时用新图(池满则 FIFO 覆盖最旧)
- 拉不到(网络波动 / 断网) → 用池里的图按顺序循环
所有取图路径(API 拉新、池出图、本地图缓存命中、本地图 fetch)在入池 / 入缓存 / 切换前都会做一次解码验证:
- 验证不过的图 → 不入池、不入缓存、不进轮播队列
- 池里历史遗留的坏图 → 首次读到时会自动从 IndexedDB 删除,并跳过该张
- 本地图解码失败 → 跳过该张,尝试下一张,不推游标
保证用户在前台永远看不到解码失败的图。
基本无感知。 缓存操作都是异步的,不阻塞页面渲染:
- 首屏图显示后,缓存 / 预取都在后台跑
- 背景图用
background-image而非<img>,不参与window.load事件 - preloader 会等待首屏背景 / Banner 图就位后再消失(上限回访 2.5s / 首次 4.5s / 硬兜底 4.5s)
后台的图片管理 / 看板娘 / 本地背景三个页面的网格里,显示的都是缩略图,不是原图。
原图可能是 4K 大图(10MB 以上),网格里只需显示 110×110 的小方块。直接加载原图会导致:
- 切 tab 时浏览器要同时解码几十张大图,主线程阻塞,感觉明显卡顿
- 内存 / GPU 占用暴涨
| 项 | 值 |
|---|---|
| 长边 | 400px(短边按比例缩放,不放大) |
| 格式 | 保持原格式(jpg / png / gif / webp) |
| 质量 | JPEG / WebP 85%,PNG 压缩级别 8 |
| 存储位置 | 与原图同层的 _thumbs 目录,同名文件 |
| 大小 | 一般 20~50KB(原图的 5%~10%) |
| 原图目录 | 缩略图目录 |
|---|---|
photos/ |
photos/thumbs/ |
photos/kanban/ |
photos/kanban_thumbs/ |
photos/localbg/ |
photos/localbg_thumbs/ |
photos/localbanner/ |
photos/localbanner_thumbs/ |
- 上传时:立即生成
- 从 API 抓取时:立即生成
- 手动放入目录:刷新后台时自动补生成
- GIF:只取第一帧作为缩略图
- 每请求上限:30 张,防止一次性生成太多把 PHP 内存打爆
生成缩略图时,程序按 Imagick → GD 顺序尝试:
- Imagick(推荐):用磁盘缓存,处理大图时几乎不吃 PHP 内存
- GD:PHP 默认扩展,处理 4000×3000 以内较稳;再大会主动跳过(避免 OOM)
- 两者都没装:不生成缩略图,网格里退回显示原图
- 上传 / 抓取 → 生成
- 删除原图 → 缩略图一并删除
- 重命名原图 → 缩略图同步重命名
- 清除数据 → 缩略图目录一并清空
- 手动删除缩略图目录 → 下次刷新后台自动重建
后台 CSS 里给网格单元加了原生性能优化:
.photo-cell {
content-visibility: auto;
contain-intrinsic-size: 0 110px;
}浏览器会自动跳过屏幕外元素的布局与绘制,缩略图多时滚动更顺滑。
window.NUOMI_CONFIG 与首图预热 fetch 提前到 <head>,比原来的 DOMContentLoaded 早 200~500ms 发起请求。预热请求超时 4 秒。
- 首次等待上限 4.5s(
FP_TIMEOUT = 4500),回访等待上限 2.5s - 硬兜底 4.5s
nuomi-first-paint事件改为等img.decode()完成才触发
__nuomiWarmupBg / __nuomiWarmupBanner 全局预热 Promise 会被首次取图直接消费。
原来 NuomiPool 的 IIFE 在 <head> 里立刻执行,此时 window.NUOMI_CONFIG 还没被赋值 → cacheLimit 永远读默认值 8。现在配置提前到 <head>。
- 并发拦截防丢失:修复了在网络极快或用户快速点击开启轮播时,并发切换导致
currentKey状态错乱,进而误删正在显示的图片的 Bug。 - Blob URL 释放:彻底修复了预取图片进入待播队列时如果发生溢出或切换失败,Blob URL 未释放导致的内存泄漏问题。
所有注入到 <script> 的 JSON 统一带 JSON_HEX_TAG:
- 把
<>转成\u003C\u003E,JSON.parse之后值完全一致 - 卡片名称、副标题、分类名、语录、看板娘名称、加载页名称/描述、UA 模板名里即使填了
</script>,也不会提前闭合页面中间注入的配置脚本 - 加载页 CSS 里正常的
.a > .b子选择器不会破坏页面结构
<head> 的预热请求加了 shouldWarmup(type) 判断,由 localStorage.nuomi_pool_full_<type> 标记驱动:
- 池未满(
'0')→ 照常预热 - 池已满(
'1')→ 跳过预热 - 本地优先 → 跳过预热
效果:日常回访流量从 4MB+ 降到接近 0,首次访问和补池体验完全不变。
- 请求超时策略:
- API 预热超时 4 秒
- 首次取图 API 超时 2.5 秒;轮播取图超时 5 秒;重试最多 3 次
- 本地图 首屏 1.5 秒、后续 2.5 秒超时
- 本地图终结死等:弱网卡住时按上述超时强制中断,立刻回退到 API 或池里的图。
- 黑名单智能判定:弱网超时不会将本地图片拉黑,只有确认损坏、404 或解码失败时才会拉黑。
- 连续超时/失败保护:
poolPickSequential连续超时 2 次即放弃、pickNextFromQueue/getLocalFallbackPick连续失败 3 次即放弃并推进 cursor。 - 首屏加载死锁解除:加载页硬兜底 4.5 秒。
- 优雅的底色兜底:无图时底色由纯黑改为预置的渐变。
- Banner 无图静默:Banner 彻底加载失败时保持完全透明。
- 超时不累加退避:
verifyDecodable/doSwitch/applyNext全部返回{ ok, timeout }结构化结果。超时不累加 failCount。 - 横竖屏切换重置退避:
orientationchange时重置bgRotator/bannerRotator的failCount。 - 后台补池不阻塞预取:
backgroundFillPool有独立bgFillPending节流标志。 - 间隔从 tick 开始计时:
scheduleNext记录tickStartAt。
admin/data.json 中的 tips 对象保存了看板娘语录配置:
{
"tips": {
"enabled": true,
"duration": 3000,
"texts": ["你好呀,我是这里的小助手~", "..."],
"periodTexts": {
"night": ["夜深了,早点休息哦 🌙", "..."],
"morning": ["早上好呀,今天也是新的一天 ☀️", "..."],
"noon": ["中午好,记得吃午饭哦 🍚", "..."],
"afternoon": ["下午好呀,喝杯水休息一下~", "..."],
"evening": ["晚上好呀,辛苦一天啦~", "..."]
},
"api": "https://v1.hitokoto.cn/?c=b",
"apiField": "hitokoto"
}
}| 字段 | 说明 |
|---|---|
enabled |
是否默认开启随机语录 |
duration |
语录显示时长(毫秒) |
texts |
通用语录数组 |
periodTexts |
分时段语录 |
api |
可选,远程语录 API 地址 |
apiField |
从 API 返回 JSON 中取哪个字段作为语录文本(默认 text) |
前台 mascot.js 采用「本地 + 远程交替出场」的策略:
- 首次显示:走本地
- 之后:本地 → 远程 → 本地 → 远程 …… 严格交替
- 远程失败或超时(1.5 秒):自动回退本地
- 未配置 API:始终走本地
https://v1.hitokoto.cn/ 的响应头没有 Access-Control-Allow-Origin,浏览器在前台直接 fetch 它会被同源策略拦截。推荐的做法:
方案一:换一个支持 CORS 的镜像
https://60s.viki.moe/v2/hitokoto(字段名hitokoto)https://hitokoto.mayx.eu.org
方案二:自己搭一个 PHP 代理
在网站根目录放一个 tips_proxy.php:
<?php
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$url = 'https://v1.hitokoto.cn/?c=b&encode=json';
$ctx = stream_context_create(['http' => ['timeout' => 5]]);
$raw = @file_get_contents($url, false, $ctx);
if ($raw === false) { echo json_encode(['error' => 'fetch failed']); exit; }
echo $raw;后台语录 API 填 https://你的域名/tips_proxy.php,字段名填 hitokoto。
- 位置记忆:右下角锚点方案,位置以
hx / hy归一化比例存储在localStorage.board_girl_pos - 自适应尺寸:根据屏幕宽高比自动缩放(横屏 56
96px,竖屏 90120px) - 长按拖动:按住 350ms 后进入拖动模式,松手保存位置
- 点击看板娘:显示 / 切换随机语录;若面板已打开则关闭面板
- 语录气泡:宽度自适应内容,中文断行优化,洗牌队列保证不连续重复,点击气泡可切换下一条
- 气泡上的齿轮按钮:点击打开控制面板
- 迷你按钮:看板娘未启用且仍有启用的 JS 特效时,前台右下角显示 ✨ 迷你按钮
- 图片加载重试:形象图加载失败时会自动重试(最多 2 次,间隔 600ms 递增),重试仍失败则降级为圆形 ✨ 悬浮按钮
- 延迟加载机制:看板娘脚本通过
preload提前下载,但执行时机被推迟到首屏加载页完全结束(page-ready)后
- 🎀 看板娘显隐开关
- ✨ JS 特效开关列表
- 💬 随机语录开关
- 👘 切换形象
- 📍 重置看板娘位置
- 将 GIF / PNG / WebP / JPG 放入
photos/kanban/,刷新后台自动识别 - 或在后台「看板娘」标签页上传
- 内置可用形象:
妹妹.gif/姐姐.gif/喵小娘.gif/月薪喵.gif/七濑胡桃.gif/舞娘娘.gif/柠檬妹.gif/熊宝宝.gif
- 编写一个纯 CSS 文件,只包含样式规则,不要有
<style>、<script>、<?php等标签。 - 文件顶部可选写注释:
/* @name 主题名称 */ /* @description 一句话描述 */
- 将文件放入
assets/preloaders/,或后台上传。 - 后台会自动识别并生成
key(文件名去扩展名)。 - 主题内可使用以下预置结构:
#preloader:全屏遮罩容器.preloader-content:内容容器.preloader-avatar:头像容器.preloader-title:标题容器.preloader-progress:进度条轨道.preloader-progress-bar:进度条填充
- 支持
html[data-theme="dark"]深色模式选择器,以及#preloader.is-revisit回访状态。 - 正常的
.a > .b子选择器可以放心使用,注入时会被安全转义。
内置主题:
sakura-spin、elegant、apple、tech、cyberpunk、ink、minimal、nature、neon、sakura、starry、vaporwave。
- 编写一个自执行函数,导出
start、stop、toggle方法:
(function () {
let running = false;
const Effect = {
start() {
if (running) return;
running = true;
},
stop() {
if (!running) return;
running = false;
},
toggle() {
running ? this.stop() : this.start();
}
};
Effect.start();
window.EffectsRegistry = window.EffectsRegistry || [];
window.EffectsRegistry.push({
name: 'myEffect',
module: Effect
});
})();- 将文件放入
assets/js/,或后台上传 / URL 添加。 - 后台会自动提取
name作为matchKey。 - 特效开关状态保存在
localStorage.effects_states。 - 加载顺序:
frontend.js→ 各特效 JS →mascot.js。 - 延迟执行:所有特效脚本都会在首屏加载页结束后(
body.page-ready)由index.php中的内联脚本动态注入执行。
| 文件 | name |
效果 | 类型 |
|---|---|---|---|
粒子微粒.js |
particledots |
粒子微粒漂浮,鼠标排斥、点击爆破 | 可控 |
天气.js |
weather |
心知天气气泡小部件 | 可控 |
雪花飘落.js |
snowfall |
雪花自上而下飘落 | 可控 |
烟花波纹.js |
firework |
点击 / 长按生成烟花粒子 | 可控 |
樱花飘落.js |
sakura |
樱花花瓣随风飘落 | 可控 |
主题系统(前台 / 后台共用同一份偏好):
- 偏好键:
localStorage.themeMode - 可选值:
auto/light/dark - 根元素属性:
html[data-theme="light"]/html[data-theme="dark"] - 防闪烁:内联解析逻辑在
<head>最前方,CSS 解析前即确定主题
其余本地存储:
- 轮播模式:
localStorage.autoLoopMode=off/on/fresh - 图片缓存(IndexedDB):数据库
nuomi_img_cache,对象仓库images - 上次预取 key:
nuomi_last_prefetch_bg/nuomi_last_prefetch_banner - 上次停留 key:
nuomi_last_shown_bg/nuomi_last_shown_banner - 顺序取池游标:
nuomi_last_pool_bg/nuomi_last_pool_banner - 本地优先队列游标:
nuomi_queue_cursor_bg/nuomi_queue_cursor_banner - 本地优先显示 ID:
nuomi_local_shown_bg/nuomi_local_shown_banner - 本地优先预取 ID:
nuomi_local_prefetch_bg/nuomi_local_prefetch_banner - 本地兜底游标:
nuomi_local_fallback_idx_bg/nuomi_local_fallback_idx_banner - 坏本地图黑名单:
nuomi_bad_local_bg_<signature>/nuomi_bad_local_banner_<signature> - 池满标记:
nuomi_pool_full_bg/nuomi_pool_full_banner='0'/'1' - 看板娘位置:
board_girl_pos - 看板娘形象索引:
board_girl_index - 看板娘语录开关:
board_girl_tip_enabled - 看板娘显隐:
board_girl_visible - 特效开关状态:
effects_states - 特效关闭 Cookie:
nuomi_effects_off - 加载页主题偏好:
preloader_style_pref - 后台当前标签:
nuomi_admin_tab - 后台站点设置子标签:
nuomi_admin_site_subtab - 首页公告条已关闭列表:
nuomi_home_ann_dismissed(JSON 数组,记录已关闭的公告 ID,最多保留 20 条)
data.json中统一存储网站根目录相对路径:photos/xxx.png、assets/js/xxx.js、photos/kanban/xxx.gif、photos/localbg/xxx.jpg。- 后台运行在
admin/子目录,PHP 渲染时会自动拼接../用于显示,保存 JSON 不会写入../。 http://或https://开头的 CDN 图片 / JS 原样使用,不做路径转换。- 前台直接使用这些根目录相对路径,不要写成
../photos/xxx。 - 默认 favicon / adminAvatar 使用不带前导
/的形式(photos/QQ.jpg),根目录和子目录部署都能正确解析。老版本存成/photos/QQ.jpg也照常使用。 - 缩略图路径不落盘,由原图路径推导。
首屏
├── 池有历史图?
│ ├── 有 → 显示 lastShownKey(秒显)
│ │ 秒显成功后 setLastShownKey(showKey)(幂等)
│ │ 确定待播槽(nextReady):
│ │ ├── lastPrefetchKey 仍在池中且不同于 showKey → 直接复用它
│ │ └── 否则 prefetchNext() 从池里取一张进待播槽
│ │ 等 preloader 消失 → 再等 1.5s → 平滑切到待播槽里的那张
│ │ 切换成功后 setLastShownKey(刚切到的那张)
│ │ 立即 prefetchNext() 再取一张进待播槽(永远有下一张)
│ └── 无 → 走 fetchWithRetry
│ ├── 预热命中 → 验证 → 入池 → 显示
│ ├── API 成功 → 验证 → 入池 → 显示
│ └── API 重试耗尽 → 本地兜底 → 空白(深灰蓝底色)
轮播中
└── 每次切换 → doSwitch 验证 → 从待播槽取图
├── 切换成功后 setLastShownKey(刚切到的那张)
├── 立即 prefetchNext() 补下一张进待播槽
└── 槽为空 → API 拉新 → 失败则从池里顺序取
首屏
├── 本地按顺序取(优先取 target === 'both' 或 target === 当前屏幕方向的图)
│ ├── 交叉兜底:当前方向没图时,自动拉取反方向的图顶上
│ ├── 缓存命中 → 秒显
│ ├── 缓存未命中 → fetch (首屏 1.5s / 后续 2.5s 超时) → 缓存 → 显示
│ └── 弱网熔断:连续超时失败后立刻回退 API
轮播中
└── 每次切换 → doSwitch 验证 → 本地按顺序取下一张
├── 切换成功后 setLocalShownId(刚切到的那张)
└── 本地全挂 → 回退 API
强制忽略后台「本地优先」设置,等同于 API 优先的轮播行为,每轮都尽量拉新图。
公告系统由三部分组成:
- 增删改查、置顶、启用 / 禁用
- 公告内容支持 HTML,不允许
<script> - 四种类型:通知 / 成功 / 警告 / 错误
- 在首页顶部(
.header之前)显示最新一条启用的公告 - 排序规则与公告页一致:置顶优先 + 创建时间倒序
- 显示内容:类型徽章 + 标题 + 「查看详情 →」链接 + 「✕」关闭按钮
- 关闭记忆:点击「✕」后,公告 ID 写入
localStorage.nuomi_home_ann_dismissed(JSON 数组,最多保留 20 条),同一条公告不再显示 - 新公告自动恢复:新公告有新的 ID,会重新显示
- 移动端:隐藏类型徽章,更紧凑
- 主题:浅色为白底 + 蓝紫左边框,深色为深灰底 + 浅紫左边框
- 公开访问,无需登录
- 只显示
enabled: true的公告 - 按「置顶优先 + 创建时间倒序」排序
- 列表风格:时间在左上、类型徽章紧随其后、标题大字、正文紧凑,接近 api.sretna.cn 的阅读体验
- 主题自动跟随
localStorage.themeMode(浅色 / 深色) - 页脚显示站点名 + Powered by
- 空状态显示「📭 暂无公告」
404.php 的「查看公告」按钮跳转到 announcement.php(使用相对路径,子目录部署兼容)。
{
"announcements": [
{
"id": "ann_xxx",
"title": "欢迎使用糯米の引导页",
"content": "<p>这是站点公告,支持简单的 HTML。</p>",
"type": "info",
"pinned": true,
"enabled": true,
"created": "2026-10-25 14:30:00",
"updated": "2026-10-25 14:30:00"
}
]
}在线升级由 admin/update.php(前端页面)+ admin/index.php 的 5 个 update_* API(后端逻辑)+ admin/.update/(工作目录)三部分组成。
| Action | 说明 |
|---|---|
update_check |
拉取 GitHub Releases 最新版本(5 分钟缓存),返回版本对比 + 更新说明 + 下载地址 + 环境检测结果 |
update_download |
下载 zip → SHA256 校验 → 解压到 admin/.update/package/,状态转 downloaded |
update_apply |
备份当前代码 → 逐个覆盖 → 成功则 success,失败自动回滚 |
update_rollback |
用最近一次备份覆盖当前代码,状态转 rolled_back |
update_status |
返回当前状态机、是否有可回滚备份、当前版本 |
idle → downloading → downloaded → applying → success
↘ failed
↘ rolled_back(手动触发)
applying超过 10 分钟自动转failedstate.json/history.json用 tmp + rename 原子写入- 并发锁
flock(LOCK_EX | LOCK_NB),拿不到锁时直接返回「已有升级进程正在进行」
- 接口:
https://api.github.com/repos/{UPDATE_GITHUB_REPO}/releases/latest - 请求头:必须带
User-Agent(GitHub 强制要求),已内置NuomiNav-Updater/1.0 - 版本号:从
tag_name读,ltrim($tag, 'vV')转成纯版本号(兼容v1.2.0和1.2.0) - 下载地址:从
assets数组里找第一个.zip结尾的browser_download_url - SHA256:如果 asset 的
digest字段是sha256:xxx格式,自动校验 - 缓存:
admin/.update/github_release_cache.json,5 分钟内不重复请求(避免触发 GitHub 60 次/小时速率限制) - 更新说明:从
body字段读(Markdown 格式,前端按纯文本展示)
update_env_check() 会在项目根目录和 admin/ 目录分别做一次「创建 → 删除」测试文件,判断 PHP 运行用户是否有写权限:
- ✅ 通过 → 返回
writable: true,前端显示「⬆ 立即升级」 - ❌ 失败 → 返回
writable: false+reason(含 PHP 运行用户 / 文件所有者),前端显示「🔗 去 GitHub 下载」
| 防护 | 说明 |
|---|---|
| 路径穿越 | 拒绝 ..、绝对路径(/ 开头)、Windows 盘符(C:) |
| 下载白名单 | 只认 github.com / raw.githubusercontent.com / objects.githubusercontent.com / codeload.github.com |
| 覆盖白名单 | 仅允许覆盖 admin/、assets/、index.php、404.php、announcement.php、analytics.php |
| 保留清单 | admin/data.json、admin/.admin.conf、admin/.update/、photos/、assets/js/、assets/preloaders/、README.md、LICENSE、CHANGELOG.md 永不覆盖 |
| 大小限制 | 下载 zip 上限 200MB(UPDATE_MAX_PACKAGE_MB),解压后总大小上限 200MB(UPDATE_MAX_EXTRACTED_MB) |
| 并发锁 | flock(LOCK_EX | LOCK_NB) |
| 原子写入 | state.json / history.json 用 tmp + rename |
| 后台 JS 隔离 | 升级页面的 JS 只调用自己的 5 个 update_* API,不直接请求 GitHub |
每次升级成功 / 回滚都会往 admin/.update/history.json 里追加一条记录(最多保留 50 条),包含:时间、从哪个版本、到哪个版本、状态、应用文件数、备份目录。
默认对接 nmrt0316/MochiNav。想改成自己的仓库:打开 admin/defaults.php,找到:
if (!defined('UPDATE_GITHUB_REPO')) {
define('UPDATE_GITHUB_REPO', 'nmrt0316/MochiNav');
}把 nmrt0316/MochiNav 改成你自己的 owner/repo 即可。
Q:未安装时访问前台会怎样?
A:会自动 302 到 /admin 弹出 4 步安装向导。
Q:后台无法保存数据?
A:检查 admin/ 目录是否有写权限,data.json 是否可写。
Q:图片上传失败?
A:检查 photos/ 目录写权限,以及 PHP 上传大小限制(默认 5MB)。
Q:为什么后台白屏,报 Cannot redeclare site_defaults()?
A:admin/index.php 里的旧版四个 *_defaults() 函数定义未删除干净。确保 admin/index.php 只 require_once __DIR__ . '/defaults.php';,不再重复定义。
Q:为什么后台白屏,报 Call to undefined function site_defaults()?
A:admin/defaults.php 未被正确 require。检查 admin/index.php 顶部有 require_once __DIR__ . '/defaults.php';。
Q:.admin.conf / data.json 能被直接下载吗?
A:Apache 用户上传 .htaccess 后自动保护。Nginx 用户需要在站点配置里手动加 location 规则(见上方「Nginx」章节)。验证:浏览器访问 /admin/data.json,应返回 403 或 404。
Q:如何替换默认的 GitHub 链接?
A:打开 admin/defaults.php,找到 'githubUrl' 和 'poweredByUrl',把 YOUR_USERNAME/YOUR_REPO 换成真实仓库地址。已安装的用户:到后台「站点设置」手动改一次即可。
Q:公告内容支持 Markdown 吗?
A:不支持。公告内容直接作为 HTML 输出(管理员信任模型),可使用 <p> / <strong> / <a> / <ul> / <li> / <code> / <pre> / <img> 等标签。<script> 标签被拒绝。
Q:404 页「查看公告」跳到哪?
A:跳转到站内 /announcement.php。需在 Apache / Nginx 中配置 ErrorDocument 404 /404.php 才能让 404.php 生效。
Q:切「图片管理 / 看板娘 / 本地背景」时还是有点卡?
A:检查:
photos/*/_thumbs/目录里是否有对应的缩略图文件- 如果没有,多刷新几次后台(每请求最多生成 30 张)
- 如果一直生不成,说明 PHP 内存限制太紧,去面板把
memory_limit调到 512M,或安装 Imagick 扩展
Q:缩略图长边可以改吗?
A:可以。打开 admin/index.php,找到 define('THUMB_MAX_SIZE', 400); 改数字即可(推荐 300~600)。
Q:为什么有些图没有缩略图,网格里显示的还是原图?
A:说明这些图太大了,超过了 PHP 能处理的内存 / 时间上限,程序主动跳过了。
Q:缩略图删了原图还能显示吗?
A:不能。缩略图是派生物,原图被删后下次刷新会清理掉对应缩略图;反过来也一样,删掉缩略图文件夹后原图照样显示。
Q:删除图片时提示"被使用中"是怎么回事?
A:说明这张图片正在被以下位置引用之一:卡片图标、卡片弹窗大图、分类图标、站点设置里的前台头像 / 网站图标 / 后台头像。删除流程分两层确认,第二层会列出所有引用位置,确认后才会真正删除(同时清空对应引用)。
Q:安装了 Imagick 后还是报内存错误?
A:检查 Imagick 是否真的在 PHP 里生效。临时放一个 phpinfo.php(内容 <?php phpinfo();)访问它,搜索 imagick,看有没有出现对应的 section。用完记得删除。
Q:看板娘不显示?
A:检查后台「看板娘」是否启用,photos/kanban/ 中是否有图片。如果形象图加载失败,会自动降级为圆形 ✨ 按钮。
Q:看板娘语录不显示?
A:检查控制面板中「随机语录」开关是否打开,或检查 localStorage.board_girl_tip_enabled 是否为 0。
Q:远程语录 API 填了但没有效果?
A:两个常见原因:
- 字段名不对:一言接口的正文在
hitokoto字段里,不是text - CORS 跨域被拦截:换用支持 CORS 的镜像,或自己搭 PHP 代理
Q:JS 特效不生效?
A:依次确认:
- JS 文件已注册到
window.EffectsRegistry,matchKey与脚本里的name一致 - 后台「JS 特效」标签页里该特效处于启用状态
- 浏览器控制台(F12)里能搜到该特效的注册对象
- 用户没有在前台控制面板里关掉过它
Q:为什么开关某些 JS 特效时会刷新页面?
A:mascot.js 按模块是否同时暴露 start 和 stop 区分两类特效:
- 可控特效(同时有
start/stop):面板里的开关直接调用方法,即时开停、不刷新 - 固定特效(无成对方法):面板里的开关走刷新路径,先写 cookie 再刷新
Q:后台手动改了 matchKey,为什么刷新后台后又被改回去了?
A:现在不会了。 当前版本只在 matchKey 为空时才自动提取(首次识别新特效),已有值永久保留。如果觉得当前值不对,点编辑弹窗里的「🔄 自动读取」按钮主动从脚本重新提取一次。
Q:点开特效开关后,特效名旁边出现红色 ⚠ 图标,开关点不动?
A:说明脚本已经加载到前台,但 EffectsRegistry 里注册的 name 与 data.json 里的 matchKey 对不上。修复步骤:去后台「✨ JS特效」标签页,点开对应特效的编辑弹窗,点「🔄 自动读取」按钮,保存后刷新前台。
Q:背景/Banner 图不换 / 卡顿?
A:检查后台「站点设置 → 前台 → 轮播接口」中:
- API 地址是否有效
- 轮播间隔是否合理(3~60 秒)
- 若拉不到新图,会从缓存池按顺序取图兜底
Q:前台一直显示本地图,不显示 API 图?
A:检查后台「🖼 本地背景」的优先级是否误设为「本地优先」,或前台轮播模式是否为 持续新图。
Q:为什么弱网下不显示本地图,而是显示了 API 图 / 渐变底色?
A:为了极速秒开,本地图的超时被严格限制在 首屏 1.5 秒 / 后续 2.5 秒。超时失败不会把本地图拉黑,只有确认 404 / 解码失败才会拉黑。
Q:为什么本地优先模式下,切图间隔有时比后台设置的要慢很多?
A:老版本有 3 个原因叠加,当前版本已修复:
- 后台补池用独立节流标志,不阻塞预取
- 超时不累加退避,仍按
interval走 scheduleNext从 tick 开始计时
Q:从 API 抓取本地背景失败?
A:可能原因:
- 站点设置里没填背景 / Banner API 地址
- 服务器无法访问外网(
allow_url_fopen关闭或防火墙) - API 本身不稳定,抓取时正好挂了
- 当前 UA 模板被 API 拒绝
Q:抓取本地背景时默认用的是什么 UA?
A:默认 背景走 ua_mobile_default(移动端,抓竖图),Banner 走 ua_desktop_default(桌面端,抓横图)。
Q:我在 PC 上抓取本地背景,抓到的都是竖屏?
A:这是默认行为——背景抓取默认走 ua_mobile_default。想让背景也抓横图,去「🎭 UA 伪装」页把背景使用的模板切到 ua_desktop_default。
Q:Banner 抓取为什么默认是横屏?
A:因为 localBannerUaId 默认是 ua_desktop_default。Banner 区域是横向轮播,只展示 desktop / both 的横图。
Q:如何让同一张图在 PC 和手机都显示?
A:手动上传宽高接近正方形的图(会自动打 both),或上传后点图片的 ✎,把 target 手动设为 both。
Q:点「自动分类」后,网格里的图片顺序会变吗?
A:不会改变 data.json 里 images[] 的原始存储顺序。只是展示层按设备方向分组显示。
Q:点「自动分类」后网格里为什么分成了三块?
A:为了让桌面端、移动端、通用三类图片一目了然。每个分组前面有一行小标题,分组内部依旧支持拖拽排序。
Q:点「自动分类」后 toast 提示是黄色 / 红色是怎么回事?
A:绿色 = 全部成功;黄色 = 部分成功(有图片读取失败);红色 = 全部失败。识别失败的图不会被改动 target。
Q:我以前抓的图没有 target 字段怎么办?
A:完全兼容。老数据缺 target 字段时自动视为 both。
Q:我手动把某张 Banner 本地图的 target 改成了 mobile,会有什么问题?
A:前台 Banner 区域优先只取 desktop 和 both 的图。如果所有 Banner 本地图都被改成 mobile,系统会退回使用全部图。
Q:本地背景上传或抓取提示重复?
A:系统会按内容 MD5 去重。
Q:轮播按钮为什么有三种状态?怎么进入第三个状态?
A:
关闭:不轮播(播放图标)开启:点击开启,按后台优先级轮播(暂停图标)持续新图:长按该按钮 3 秒进入,强制 API 优先
Q:池满后回访还会请求 API 吗?
A:不会。<head> 预热会读 localStorage.nuomi_pool_full_<type>,池满时直接跳过预热。
Q:为什么清空浏览器数据后,第一次刷新没有预热?
A:localStorage 的标记被清空,但 IndexedDB 的池也同时被清空,此时 init() 发现池空会走 fetchWithRetry() 直接拉图,功能不受影响。
Q:本地图占多少空间?
A:每张按 500KB 估算,若背景 20 张 + Banner 20 张,约 20MB。
Q:想清空本地图的浏览器缓存怎么办?
A:F12 → Application → IndexedDB → nuomi_img_cache → images,删除以 bg_local: / banner_local: 开头的键。
Q:本地图换了新文件,前台还显示旧图?
A:缓存 key 是 filepath,改文件名 → 新 key → 前台下次走新路径拉图。
Q:为什么切到本地优先后,池里之前的 API 图还占着空间?
A:API 池的 FIFO 只在下次入池时触发淘汰。
Q:图片缓存占多少空间?
A:每个池上限由 cacheLimit 控制(默认 8),背景 / Banner 各一个池。按单图 500KB 估算,8 张两池约 8MB。
Q:想清空图片缓存怎么办?
A:直接在浏览器里「清除站点数据」,或把后台缓存数量改小,刷新前台后 FIFO 会自动淘汰超出的旧图。
Q:前台和后台的深色/浅色模式不同步?
A:正常情况下两者共用 localStorage.themeMode 键。若发现不同步,检查:
- 浏览器是否阻止了 localStorage 写入
- 前后台是否用了不同域名 / 子域名
- 是否在两个标签页同时打开(切换后需要刷新另一侧才同步)
Q:电脑端切换分类时为什么会先"上滚一下"?
A:这是预期行为。当从"卡片多"的分类切到"卡片少"的分类时,页面新高度会变短。现在锁住文档高度,让滚动位置与卡片高度、垂直居中 margin 一起走 620ms 缓动。
Q:为什么在手机端长按页面底部,不会再出现奇怪的选字光标或深色方块了?
A:在 style.css 中,精准锁定了 #app、.bg-stage 等纯 UI 容器的 user-select: none;。同时 .app-name 等文字类元素依然保留了 user-select: text;。
Q:卡片名 / 语录里填了 <script> 会怎样?
A:会被安全转义成 \u003Cscript\u003E,不会执行,也不会破坏页面结构。
Q:后台拖拽排序后点击其他按钮没反应?
A:旧版本会在拖拽结束后的 400ms 内拦截整个页面的点击,导致误吞。现在已修复:只拦截拖拽元素内部的点击。
Q:忘记后台密码?
A:删除 admin/.admin.conf,重新访问后台进行初始化。
Q:清除数据的确认词是什么?
A:指定内容 / 清除全部输入 RESET;完全清空输入 CLEAR。
Q:清除数据会清掉 UA 模板 / 公告吗?
A:指定内容模式勾选「UA 模板」会清除自定义模板,仅保留内置模板;勾选「公告」会清空所有公告。清除全部 / 完全清空也会重置或清空 UA 配置与公告。
Q:内置 UA 模板能删除吗?
A:不能,只能编辑内容。自定义模板可以删除,删除后若正在使用会自动回退默认模板。
Q:为什么 404 页面主题和前台不同步?
A:404.php 使用独立的 localStorage.theme,前台和后台使用 localStorage.themeMode。
Q:如何让本地 JS 特效被后台识别?
A:直接把 .js 文件放入 assets/js/,刷新后台页面即可自动扫描。
Q:加载页主题不生效?
A:检查:
- 后台「加载页」标签页中,主题是否已启用
assets/preloaders/目录下是否存在对应的.css文件- CSS 文件是否包含非法标签
- 前台用户是否在底部加载动画按钮中选择过其他主题
- 站点设置中首屏加载动画开关是否开启
Q:为什么上传的加载页 CSS 没有出现在列表中?
A:后台只识别 assets/preloaders/ 下的 .css 文件,且文件内容不能包含 PHP 标签或 </style>、<script、</script。
Q:preloader 是什么?
A:preloader 是首屏加载动画——页面打开时先显示的那个全屏遮罩。它会等首屏背景 / Banner 图就位后再消失。
Q:全站统计代码有什么限制?
A:只有三条:
- 必须包含 HTML 标签
- 不能包含
<?php或<?= - 无长度限制
Q:统计代码为什么没在前台生效?
A:检查:
- 后台「🌐 统计代码」标签页里是否已保存内容
- 前台页面 HTML 源码的
</body>前是否有你保存的代码 - 浏览器 F12 Console 是否有报错
- 是否被 CDN 的边缘缓存了旧版本 HTML
Q:Cloudflare 已经在自动注入统计代码了,还能在后台填吗?
A:不要同时填。二选一。
Q:本地兜底图会不会拖慢轮播?
A:不会。本地图只在 API 重试全败后启用,且首次加载后会写入 IndexedDB 缓存。
Q:解码验证会不会消耗性能?
A:不会。每张图多一次 new Image() + onload,浏览器命中内存解码缓存,实测 <5ms。
Q:为什么我点了后台的"自动分类",前台还是有一些图不显示?
A:自动分类会按图片的宽高比例自动打上 desktop 或 mobile 标签。如果当前是电脑端,且后台全是竖图,前台会触发交叉兜底把竖图拉过来显示。
Q:为什么生产环境访问出错时看不到 PHP 报错详情?
A:admin/index.php 顶部已关闭 display_errors 并开启 log_errors,PHP 报错只写入服务器错误日志。排查问题时去 PHP 错误日志找,或临时把 display_errors 改回 '1' 调试完再改回来。
Q:子目录部署时默认图标为什么会 404?
A:老版本 favicon / adminAvatar 默认值写的是 /photos/QQ.jpg(带前导 /)。当前版本默认值已改为 photos/QQ.jpg(不带前导 /),根目录和子目录部署都能正确解析。
Q:删除图片时如何避免误删被引用的图?
A:删除流程有两层确认:
- 第一层(常规确认):防误点
- 第二层(引用确认):如果图片正在被引用,后端会拒绝首次删除并返回引用列表,前端自动弹第二个确认框
Q:数据文件损坏怎么办?
A:从备份恢复,或临时把 admin/data.json 重命名为 data.json.bak,系统会重建空数据。
Q:如何修改版本号?
A:打开 admin/defaults.php,修改 define('NUOMI_VERSION', '1.1.0'); 这一行。后台顶部栏会自动显示新版本。
Q:升级时保留哪些文件?
A:见上方「升级须知」章节。必须保留:admin/data.json、admin/.admin.conf、admin/.update/、photos/、assets/js/、assets/preloaders/。
Q:.gitignore 里为什么排除了 photos/*?
A:因为 photos/ 里大多数是用户上传的图片,不应提交到仓库。photos/QQ.jpg、photos/zy.png、photos/lx.png 通过 ! 白名单保留。
Q:首页公告条怎么关闭?关闭后还能恢复吗?
A:点击公告条右侧的「✕」即可关闭。关闭后会记录在 localStorage.nuomi_home_ann_dismissed 里,同一条公告不再显示。如果后台修改了公告内容(ID 不变)仍然不显示;如果是新公告(新 ID),会重新显示。想恢复某条已关闭的公告,清除浏览器站点数据即可。
Q:首页公告条和公告页显示的是同一条公告吗?
A:是的,首页公告条只取最新一条启用的公告(置顶优先 + 创建时间倒序),公告页显示所有启用的公告。如果有多条公告,首页只展示第一条,其余需到 announcement.php 查看。
Q:公告页为什么从卡片风格改成了列表风格?
A:列表风格更接近主流公告页的阅读体验——时间在左上、标题大字、正文紧凑,长文阅读不累。4 种类型仍通过左侧徽章和颜色区分。
Q:怎么进入在线升级页?
A:后台顶部栏(登录后)点「版本号(如 v1.1.0)」胶囊按钮,或直接访问 https://你的域名/admin/update.php。
Q:在线升级提示「环境不支持」怎么办?
A:说明 PHP 运行用户无法在项目根目录创建 / 覆盖 / 删除文件。常见于共享主机(PHP 运行用户 www-data ≠ 文件所有者 nobody)。三种解决方案:
- 让主机商把项目目录所有者改为 PHP 运行用户
- 给项目目录设置
775权限并确保两者在同一用户组 - 直接点「🔗 去 GitHub 下载」按钮,手动覆盖升级
Q:在线升级会不会覆盖我的 data.json / 照片?
A:不会。升级过程会自动跳过 admin/data.json、admin/.admin.conf、admin/.update/、photos/、assets/js/、assets/preloaders/、README.md、LICENSE、CHANGELOG.md,即使 Release 包里带了这些路径也不会覆盖。
Q:在线升级失败了怎么办?
A:升级失败会自动回滚到升级前的状态。如果回滚也失败,可以手动点「↩ 回滚到上一版本」用最近一次备份覆盖。备份默认保留最近 3 份(UPDATE_BACKUP_KEEP),存放在 admin/.update/backup/<时间戳>/。
Q:升级成功但顶部栏版本号还是旧版本?
A:升级完成后按 Ctrl+F5 强制刷新后台,让浏览器加载新版本的 admin.js / admin.css。如果还是旧版本,说明服务器/CDN 缓存了旧 HTML,需要清除缓存。
Q:在线升级请求 GitHub API 会不会触发速率限制?
A:不会。update_check 有 5 分钟本地缓存(admin/.update/github_release_cache.json),5 分钟内重复检查不会重新请求 GitHub。GitHub 未认证请求的速率限制是 60 次/小时,正常使用完全够。
Q:能配置成自己的 GitHub 仓库吗?
A:可以。打开 admin/defaults.php,找到 UPDATE_GITHUB_REPO 常量,把 nmrt0316/MochiNav 改成你自己的 owner/repo 即可。
Q:升级后想回退到旧版本怎么办?
A:进 admin/update.php,点「↩ 回滚到上一版本」按钮,会用最近一次备份覆盖当前代码。注意:回滚只回滚代码,不回滚 data.json / photos/(这些升级过程本来就没动过)。
糯米の引导页 · 让导航页也充满仪式感 ✨