Files
HomeDash/web/extension/install.html
JianFeeeee f8c4771828 fix(admin,theme): make admin actually work; glass theme targets real DOM
后台之前有两处让它完全不可用的问题(都属于「看着有、实际不工作」):
1. 写操作 401 —— admin UI 从未带 Authorization 头,而 admin-server 强制校验
   NAVI_ADMIN_TOKEN。于是添加/删除一直静默失败。
2. JSON 键名不匹配 —— Section/NavItem 只有 yaml tag,没有 json tag,
   接口返回 {"Name":..,"Items":[{"Title":..}]},而前端读 s.name / it.title,
   渲染直接抛异常(菜单区永远停在「加载中…」)。

修法:
- admin-server:token 改为**默认不校验**(/admin/ 已在外层门户登录墙内,
  用户确认无需叠加),仅当显式设 NAVI_ADMIN_REQUIRE_TOKEN=1 时才校验;
  历史 DELETE 不受影响(那条路径被门户放行,仍必须自带 token)。
- 补 json tag;Config/Section/NavItem 加 yaml ",inline" 保留未知键,
  避免保存 conf.yml 时把不认识的配置(layout/pages/item 的 target 等)丢掉。
- 新增 /api/pageinfo(标题/副标题/页脚/顶部导航可在后台编辑)与
  /api/icon(图标上传,限 png/svg/jpg,512KB)。
- 重写 admin UI:站点信息 + 顶部导航编辑、图标上传(.ico 用 canvas 转 PNG,
  因为 Dashy 的 isImage() 只认 svg/png/jpg)、可选令牌。

主题:旧 customCss 大部分是死代码 —— .tile / .item-group / .item-group-outer
在页面里根本不存在(querySelectorAll 计数为 0),卡片灰底其实来自 a.item,
而 .home 的不透明背景盖住了 body 渐变,所以玻璃效果一直看不出来。
现按真实类名(a.item / .item-wrapper / .collapsable / .collapsible-content /
.home / header)重写,并加入工作空间侧边栏的溢出修复。

安装页:.btn 自带 display 会盖掉浏览器默认的 [hidden]{display:none},
未配置商店时「一键安装」按钮仍然可见且 href 为空(点了没反应)。
现显式兜住 [hidden],并把浏览器识别补全(iOS/Android 单独提示为不可安装、
Edge/Brave/Vivaldi/Opera 给出各自的 extensions 地址、优先用 userAgentData)。
2026-09-11 11:43:44 +08:00

243 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<title>安装浏览历史插件</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
/* .btn 自己设了 display,会盖掉浏览器默认的 [hidden]{display:none},
导致未配置商店时「一键安装」按钮仍然可见且 href 为空 —— 必须显式兜住 */
[hidden]{display:none !important}
body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
background:
radial-gradient(1000px 500px at 10% -10%, #1e3a8a 0%, transparent 60%),
radial-gradient(900px 500px at 90% 0%, #312e81 0%, transparent 55%),
#0f172a;
background-attachment:fixed;color:#e2e8f0;line-height:1.6;padding:32px 18px 64px}
.wrap{max-width:760px;margin:0 auto}
h1{font-size:24px;font-weight:700;margin-bottom:6px}
.sub{color:#94a3b8;font-size:14px;margin-bottom:24px}
.card{background:rgba(30,41,59,.72);border:1px solid #334155;border-radius:16px;
padding:20px 22px;margin-bottom:16px;
backdrop-filter:blur(14px) saturate(150%);-webkit-backdrop-filter:blur(14px) saturate(150%);
box-shadow:0 10px 30px rgba(0,0,0,.28)}
.card h2{font-size:17px;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.badge{font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;
background:#1e3a5f;color:#93c5fd;border:1px solid #2563eb;white-space:nowrap}
.badge.warn{background:#3f2d17;color:#fcd34d;border-color:#b45309}
.card.current{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb inset,0 10px 30px rgba(0,0,0,.28)}
ol{margin:0 0 0 20px;font-size:14px;color:#cbd5e1}
ol li{margin-bottom:7px}
a{color:#60a5fa}
code{background:#0b1220;border:1px solid #334155;border-radius:5px;padding:1px 6px;font-size:13px}
.note{font-size:13px;color:#94a3b8;margin-top:12px;border-top:1px solid #334155;padding-top:12px}
.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:14px}
.btn{display:inline-block;padding:9px 18px;border-radius:10px;font-size:14px;font-weight:600;
text-decoration:none;border:1px solid #334155;background:#0b1220;color:#e2e8f0;cursor:pointer}
.btn.primary{background:#2563eb;border-color:#2563eb;color:#fff}
.btn.primary:hover{background:#1d4ed8}
.site{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px}
.site input{flex:1;min-width:240px;background:#0b1220;border:1px solid #334155;border-radius:10px;
padding:9px 12px;color:#e2e8f0;font-size:14px}
.hint{font-size:13px;color:#94a3b8;margin-top:10px}
.done{font-size:13px;color:#86efac;margin-top:8px}
</style>
</head>
<body>
<div class="wrap">
<h1>安装浏览历史插件</h1>
<p class="sub">安装后浏览记录会自动同步到本站,并据此把最常访问的服务排到「常用服务」最前面。</p>
<div class="card">
<h2>第一步:填写本站地址</h2>
<p class="hint">在插件设置里粘贴下面这个地址(点「复制」)。同步令牌从部署方的 <code>NAVI_SYNC_TOKEN</code> 环境变量获取。</p>
<div class="site">
<input id="site-url" readonly>
<button class="btn" id="copy-btn" type="button">复制</button>
</div>
<div class="done" id="copy-done" hidden>已复制</div>
</div>
<div class="card" id="card-chromium" hidden>
<h2>Chrome / Edge / 其他 Chromium <span class="badge">已识别你的浏览器</span></h2>
<div class="actions">
<a class="btn primary" id="store-chromium" hidden>从商店一键安装</a>
<a class="btn" href="/extension/navi-chrome.zip" download id="dl-chromium">下载扩展包(zip)</a>
</div>
<ol>
<li>解压下载的 zip 到一个固定目录(不要删除)</li>
<li>地址栏打开 <code id="ext-url">chrome://extensions</code></li>
<li>打开右上角「开发者模式」</li>
<li>点「加载已解压的扩展程序」,选择解压出来的目录</li>
<li>点扩展图标,填入上面的本站地址与同步令牌,保存</li>
</ol>
<p class="note" id="note-chromium"></p>
</div>
<div class="card" id="card-firefox" hidden>
<h2>Firefox <span class="badge">已识别你的浏览器</span></h2>
<div class="actions">
<a class="btn primary" id="store-firefox" hidden>从商店一键安装</a>
<a class="btn" href="/extension/navi-firefox.xpi" download>下载扩展包(xpi)</a>
</div>
<ol>
<li>地址栏打开 <code>about:debugging#/runtime/this-firefox</code></li>
<li>点「临时加载附加组件」,选择下载的 xpi</li>
<li>点扩展图标,填入上面的本站地址与同步令牌,保存</li>
</ol>
<p class="note" id="note-firefox"></p>
</div>
<div class="card" id="card-mobile" hidden>
<h2>手机浏览器 <span class="badge warn">此环境无法安装扩展</span></h2>
<p class="hint">iOS 上所有浏览器都只能用 Safari 内核,且 Safari 扩展需要 Xcode 打包的 App 才能安装;
Android 版 Chrome 也不支持安装扩展。所以手机上无法完成这一步。</p>
<div class="actions">
<a class="btn" href="/extension/navi-chrome.zip" download>下载 Chromium 版(zip)</a>
<a class="btn" href="/extension/navi-firefox.xpi" download>下载 Firefox 版(xpi)</a>
</div>
<p class="note">请在电脑上打开本页面完成安装。手机上浏览产生的记录暂不会同步,但电脑端安装后仍会统计。</p>
</div>
<div class="card" id="card-other" hidden>
<h2>其他浏览器</h2>
<p class="hint">未能识别浏览器内核。Chromium 系(Brave、Vivaldi、Opera、Kiwi 等)请按上面 Chrome 的步骤操作;
Firefox 系请按 Firefox 的步骤操作。</p>
<div class="actions">
<a class="btn" href="/extension/navi-chrome.zip" download>下载 Chromium 版(zip)</a>
<a class="btn" href="/extension/navi-firefox.xpi" download>下载 Firefox 版(xpi)</a>
</div>
</div>
<div class="card">
<h2>装好后</h2>
<ol>
<li>回到导航页并刷新,本次会话浏览几条页面</li>
<li>约 30 秒后历史会自动上报,「历史访问」开始按访问频次排序</li>
<li>检测到插件已安装后,本入口会自动隐藏</li>
</ol>
</div>
</div>
<script>
(function () {
'use strict';
document.getElementById('site-url').value = location.origin;
document.getElementById('copy-btn').addEventListener('click', function () {
var input = document.getElementById('site-url');
input.select();
input.setSelectionRange(0, 999);
var done = document.getElementById('copy-done');
function ok() { done.hidden = false; setTimeout(function () { done.hidden = true; }, 2000); }
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(input.value).then(ok, function () { document.execCommand('copy'); ok(); });
} else {
document.execCommand('copy'); ok();
}
});
/* ── 浏览器识别 ──
UA 会互相包含(Edge/Brave/Opera 的 UA 里都有 Chrome/),所以要按
「更具体的先判」的顺序来。
另外 iOS 上所有浏览器都是 WebKit 内核,且 Safari 扩展必须由 App 打包,
根本装不了 —— 必须单独识别,否则会给用户一套做不到的桌面步骤。 */
function detect() {
var ua = navigator.userAgent;
var uad = navigator.userAgentData || null;
var brands = (uad && uad.brands ? uad.brands : []).map(function (b) { return b.brand; });
var brandStr = brands.join(' ');
var isIOS = /iPad|iPhone|iPod/.test(ua) ||
(/Macintosh/.test(ua) && navigator.maxTouchPoints > 1);
var isAndroid = /Android/i.test(ua);
var isFirefox = /firefox\/|fxios\//i.test(ua);
var isEdge = /edg\//i.test(ua) || brands.indexOf('Microsoft Edge') >= 0;
var isOpera = /opr\//i.test(ua) || brands.indexOf('Opera') >= 0;
var isBrave = !!(navigator.brave && navigator.brave.isBrave);
var isVivaldi = /vivaldi/i.test(ua);
var isChromium = !isFirefox && (
/chrome\/|chromium\//i.test(ua) ||
brands.some(function (b) { return /Chromium|Google Chrome/i.test(b); })
);
return {
isIOS: isIOS, isAndroid: isAndroid,
isMobile: isIOS || isAndroid || /Mobile|Tablet/i.test(ua),
isFirefox: isFirefox, isEdge: isEdge, isOpera: isOpera,
isBrave: isBrave, isVivaldi: isVivaldi, isChromium: isChromium,
isSafari: /safari\//i.test(ua) && !isChromium && !isFirefox,
brand: brandStr
};
}
var d = detect();
var card;
if (d.isFirefox && !d.isIOS) {
card = document.getElementById('card-firefox');
} else if (d.isMobile) {
card = document.getElementById('card-mobile');
} else if (d.isChromium) {
card = document.getElementById('card-chromium');
var extUrl = 'chrome://extensions';
var label = 'Chrome';
if (d.isEdge) { extUrl = 'edge://extensions'; label = 'Edge'; }
else if (d.isBrave) { extUrl = 'brave://extensions'; label = 'Brave'; }
else if (d.isVivaldi) { extUrl = 'vivaldi://extensions'; label = 'Vivaldi'; }
else if (d.isOpera) { extUrl = 'opera://extensions'; label = 'Opera'; }
document.getElementById('ext-url').textContent = extUrl;
if (label !== 'Chrome') {
document.querySelector('#card-chromium h2').firstChild.textContent =
label + '(Chromium 内核) ';
}
} else {
card = document.getElementById('card-other');
}
card.hidden = false;
card.classList.add('current');
/* 商店链接是可选的:部署方在 data 卷放 extension-store.json 才启用。
本实例没有发布到任何商店,所以默认把按钮整个移除 —— 避免出现
点不动的「一键安装」。 */
fetch('/extension/store.json', { cache: 'no-store' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (s) {
var hasChrome = s && s.chrome;
var hasFirefox = s && s.firefox;
if (hasChrome) showStore('store-chromium', s.chrome);
if (hasFirefox) showStore('store-firefox', s.firefox);
setNotes(hasChrome, hasFirefox);
})
.catch(function () { setNotes(false, false); });
function showStore(id, url) {
var el = document.getElementById(id);
if (!el) return;
el.href = url;
el.hidden = false;
}
function setNotes(fromChromeStore, fromFirefoxStore) {
var c = document.getElementById('note-chromium');
var f = document.getElementById('note-firefox');
if (c) {
c.textContent = fromChromeStore
? '也可以从上面的商店按钮一键安装。'
: '本实例未发布到浏览器商店,Chrome/Edge 不允许从网站直接安装非商店扩展,因此只能按上面步骤手动加载。';
}
if (f) {
f.textContent = fromFirefoxStore
? '也可以从上面的商店按钮一键安装。'
: '本实例未发布到商店;Firefox 正式版只接受已签名的附加组件,未签名的 xpi 只能在开发者版 / ESR 上加载,或在 about:config 关闭 xpinstall.signatures.required。';
}
}
})();
</script>
</body>
</html>