
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>网址书签</title>
<style>
html, body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: transparent;
}
* {
box-sizing: border-box;
}
body {
font-family: "Microsoft YaHei", sans-serif;
color: rgba(238, 246, 255, .94);
}
.card {
width: 100%;
height: 100%;
box-sizing: border-box;
border-radius: 18px;
overflow: hidden;
position: relative;
padding: 14px;
background:
radial-gradient(circle at 12% 0%, rgba(95, 170, 255, .22), transparent 34%),
radial-gradient(circle at 86% 8%, rgba(109, 92, 255, .18), transparent 34%),
linear-gradient(145deg, rgba(14, 21, 34, .96), rgba(9, 13, 24, .98));
border: 1px solid rgba(136, 195, 255, .26);
box-shadow:
inset 0 1px 0 rgba(255,255,255,.10),
inset 0 -1px 0 rgba(0,0,0,.18);
}
.card::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border-radius: 18px;
background: linear-gradient(180deg, rgba(255,255,255,.075), transparent 34%);
}
.main {
position: relative;
z-index: 1;
height: 100%;
display: grid;
grid-template-rows: auto auto auto 1fr;
gap: 10px;
min-height: 0;
}
.top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.title {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.logo {
width: 28px;
height: 28px;
border-radius: 10px;
display: grid;
place-items: center;
background: linear-gradient(145deg, rgba(98, 176, 255, .26), rgba(92, 104, 255, .15));
border: 1px solid rgba(142, 203, 255, .25);
box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
font-size: 15px;
}
.titleText {
min-width: 0;
}
.titleText strong {
display: block;
font-size: 14px;
letter-spacing: .5px;
line-height: 17px;
}
.titleText span {
display: block;
font-size: 11px;
color: rgba(198, 218, 240, .56);
line-height: 15px;
white-space: nowrap;
}
.status {
font-size: 11px;
color: rgba(188, 213, 242, .62);
white-space: nowrap;
max-width: 168px;
overflow: hidden;
text-overflow: ellipsis;
}
.searchLine {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 8px;
}
.addLine {
display: none;
grid-template-columns: 1.15fr 1.75fr auto;
gap: 8px;
}
.addLine.show {
display: grid;
}
.hintLine {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 16px;
padding: 0 2px;
margin-top: -2px;
color: rgba(198, 218, 240, .42);
font-size: 10px;
line-height: 14px;
}
.hintLine span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hintLine b {
color: rgba(198, 225, 255, .68);
font-weight: 500;
}
input {
width: 100%;
height: 34px;
min-width: 0;
border: 1px solid rgba(142, 203, 255, .20);
outline: none;
border-radius: 12px;
padding: 0 11px;
color: rgba(241, 248, 255, .96);
background: rgba(4, 9, 18, .45);
box-shadow: inset 0 1px 0 rgba(255,255,255,.045);
font-family: "Microsoft YaHei", sans-serif;
font-size: 12px;
}
input::placeholder {
color: rgba(197, 219, 244, .42);
}
input:hover {
border-color: rgba(142, 203, 255, .36);
background: rgba(7, 14, 27, .58);
}
input:focus {
border-color: rgba(116, 190, 255, .68);
background: rgba(9, 18, 34, .68);
box-shadow:
0 0 0 3px rgba(81, 159, 255, .12),
inset 0 1px 0 rgba(255,255,255,.06);
}
button {
height: 34px;
border: 1px solid rgba(142, 203, 255, .22);
border-radius: 12px;
padding: 0 12px;
color: rgba(239, 247, 255, .94);
background: linear-gradient(180deg, rgba(78, 145, 222, .22), rgba(43, 82, 148, .20));
font-family: "Microsoft YaHei", sans-serif;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
user-select: none;
transition: transform .08s ease, border-color .12s ease, background .12s ease, opacity .12s ease;
}
button:hover {
border-color: rgba(151, 211, 255, .50);
background: linear-gradient(180deg, rgba(90, 169, 255, .32), rgba(62, 105, 184, .27));
}
button:active {
transform: translateY(1px) scale(.985);
background: linear-gradient(180deg, rgba(63, 123, 205, .30), rgba(35, 70, 134, .30));
}
button.ghost {
background: rgba(255,255,255,.055);
}
button.danger:hover {
border-color: rgba(255, 135, 135, .52);
background: rgba(255, 80, 80, .16);
}
.list {
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-right: 3px;
}
.list::-webkit-scrollbar {
width: 6px;
}
.list::-webkit-scrollbar-track {
background: rgba(255,255,255,.035);
border-radius: 99px;
}
.list::-webkit-scrollbar-thumb {
background: rgba(145, 191, 236, .26);
border-radius: 99px;
}
.list::-webkit-scrollbar-thumb:hover {
background: rgba(145, 205, 255, .42);
}
.item {
height: 38px;
display: grid;
grid-template-columns: 24px 30px 1fr 30px 30px;
gap: 6px;
align-items: center;
border-radius: 13px;
padding: 4px 5px;
border: 1px solid transparent;
cursor: pointer;
user-select: none;
transition: background .12s ease, border-color .12s ease, opacity .12s ease, transform .12s ease;
}
.item:hover {
background: rgba(255,255,255,.055);
border-color: rgba(142, 203, 255, .12);
}
.item:active {
background: rgba(96, 165, 250, .10);
}
.item.dragging {
opacity: .42;
transform: scale(.985);
border-color: rgba(142, 203, 255, .30);
background: rgba(96, 165, 250, .12);
}
.item.dropTop {
box-shadow: inset 0 2px 0 rgba(125, 197, 255, .72);
}
.item.dropBottom {
box-shadow: inset 0 -2px 0 rgba(125, 197, 255, .72);
}
.dragHandle {
width: 24px;
height: 28px;
display: grid;
place-items: center;
border-radius: 9px;
color: rgba(198, 218, 240, .40);
font-size: 15px;
cursor: grab;
}
.dragHandle:hover {
color: rgba(223, 240, 255, .82);
background: rgba(255,255,255,.055);
}
.dragHandle:active {
cursor: grabbing;
}
.favicon {
width: 28px;
height: 28px;
border-radius: 10px;
display: grid;
place-items: center;
color: rgba(230, 242, 255, .92);
background: linear-gradient(145deg, rgba(88, 160, 255, .25), rgba(99, 95, 255, .15));
border: 1px solid rgba(160, 212, 255, .18);
font-size: 13px;
}
.meta {
min-width: 0;
}
.name {
font-size: 12px;
line-height: 16px;
color: rgba(242, 248, 255, .96);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.url {
font-size: 10px;
line-height: 14px;
color: rgba(188, 213, 242, .52);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.iconBtn {
width: 30px;
height: 28px;
padding: 0;
border-radius: 10px;
font-size: 13px;
opacity: .62;
cursor: pointer;
}
.item:hover .iconBtn {
opacity: 1;
}
.editPanel {
position: absolute;
z-index: 10;
left: 14px;
right: 14px;
bottom: 14px;
display: none;
grid-template-columns: 1.05fr 1.65fr auto auto;
gap: 8px;
padding: 10px;
border-radius: 16px;
border: 1px solid rgba(142, 203, 255, .24);
background:
radial-gradient(circle at 12% 0%, rgba(95, 170, 255, .18), transparent 42%),
linear-gradient(145deg, rgba(10, 17, 30, .96), rgba(5, 9, 18, .97));
box-shadow:
0 12px 34px rgba(0,0,0,.30),
inset 0 1px 0 rgba(255,255,255,.08);
}
.editPanel.show {
display: grid;
}
.empty {
height: 100%;
min-height: 78px;
display: grid;
place-items: center;
text-align: center;
color: rgba(198, 218, 240, .55);
font-size: 12px;
line-height: 20px;
}
.empty b {
color: rgba(238, 247, 255, .86);
font-weight: 500;
}
@media (max-width: 460px) {
.addLine {
grid-template-columns: 1fr 1fr auto;
}
.status {
display: none;
}
button {
padding: 0 10px;
}
.item {
grid-template-columns: 22px 30px 1fr 28px 28px;
gap: 5px;
}
.editPanel {
grid-template-columns: 1fr 1fr auto auto;
}
}
</style>
</head>
<body>
<div class="card">
<div class="main">
<div class="top">
<div class="title">
<div class="logo">↗</div>
<div class="titleText">
<strong>网址书签</strong>
<span>搜索、收藏、快速跳转</span>
</div>
</div>
<div class="status" id="status">内容已就绪</div>
</div>
<div class="searchLine">
<input id="searchInput" type="text" placeholder="搜索书签,或输入关键词 / 网址后回车">
<button id="searchBtn">搜索</button>
<button id="toggleAddBtn" class="ghost">添加</button>
</div>
<div class="addLine" id="addLine">
<input id="nameInput" type="text" placeholder="名称">
<input id="urlInput" type="text" placeholder="网址,例如 https://example.com">
<button id="saveBtn">保存</button>
</div>
<div class="hintLine">
<span>拖住 <b>⋮⋮</b> 可自由排序,点 <b>✎</b> 修改名称或网址</span>
<span id="countText"></span>
</div>
<div class="list" id="list"></div>
<div class="editPanel" id="editPanel">
<input id="editNameInput" type="text" placeholder="名称">
<input id="editUrlInput" type="text" placeholder="网址">
<button id="editSaveBtn">保存</button>
<button id="editCancelBtn" class="ghost">取消</button>
</div>
</div>
</div>
<script>
(function () {
var stateKey = "bookmarks";
var fallbackKey = "fudao_bookmarks_fallback";
var hostReady = false;
var editingId = "";
var dragId = "";
var dropId = "";
var dropPos = "";
var bookmarks = [
{ id: makeId(), name: "百度", url: "https://www.baidu.com" },
{ id: makeId(), name: "必应", url: "https://www.bing.com" },
{ id: makeId(), name: "知乎", url: "https://www.zhihu.com" },
{ id: makeId(), name: "GitHub", url: "https://github.com" }
];
var listEl = document.getElementById("list");
var statusEl = document.getElementById("status");
var countText = document.getElementById("countText");
var searchInput = document.getElementById("searchInput");
var searchBtn = document.getElementById("searchBtn");
var toggleAddBtn = document.getElementById("toggleAddBtn");
var addLine = document.getElementById("addLine");
var nameInput = document.getElementById("nameInput");
var urlInput = document.getElementById("urlInput");
var saveBtn = document.getElementById("saveBtn");
var editPanel = document.getElementById("editPanel");
var editNameInput = document.getElementById("editNameInput");
var editUrlInput = document.getElementById("editUrlInput");
var editSaveBtn = document.getElementById("editSaveBtn");
var editCancelBtn = document.getElementById("editCancelBtn");
function makeId() {
return "b_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 8);
}
function setStatus(text) {
statusEl.textContent = text || "";
}
function hasHost() {
return !!(window.fudao && typeof window.fudao.invoke === "function");
}
function invoke(method, args) {
if (!hasHost()) {
return Promise.reject(new Error("fudao not ready"));
}
return window.fudao.invoke(method, args || {});
}
function normalizeUrl(input) {
var s = (input || "").trim();
if (!s) return "";
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(s)) return s;
if (/^[a-zA-Z]:\\/.test(s)) return s;
if (s.indexOf(".") > 0 && !/\s/.test(s)) return "https://" + s;
return "";
}
function searchUrl(q) {
return "https://www.bing.com/search?q=" + encodeURIComponent(q);
}
function getHost(url) {
try {
return new URL(url).hostname.replace(/^www\./, "");
} catch (e) {
return (url || "").replace(/^https?:\/\//, "").split("/")[0] || url;
}
}
function getInitial(name, url) {
var s = (name || getHost(url) || "?").trim();
return s ? s.charAt(0).toUpperCase() : "?";
}
function normalizeBookmarks(data) {
var arr = Array.isArray(data) ? data : [];
var result = [];
var seen = {};
arr.forEach(function (x) {
if (!x) return;
var url = normalizeUrl(x.url || "");
if (!url) return;
var id = (x.id || "").toString();
if (!id || seen[id]) id = makeId();
seen[id] = true;
result.push({
id: id,
name: (x.name || getHost(url)).toString(),
url: url
});
});
return result.length ? result : bookmarks;
}
function safeJsonParse(text, fallback) {
try {
var data = JSON.parse(text);
return normalizeBookmarks(data);
} catch (e) {
return fallback;
}
}
function loadFallback() {
try {
var saved = localStorage.getItem(fallbackKey);
if (saved) bookmarks = safeJsonParse(saved, bookmarks);
} catch (e) {}
}
function saveFallback() {
try {
localStorage.setItem(fallbackKey, JSON.stringify(bookmarks));
} catch (e) {}
}
function saveState(reason) {
saveFallback();
if (!hostReady) {
setStatus(reason || "已暂存到本地兜底");
return;
}
invoke("state.write", {
key: stateKey,
value: JSON.stringify(bookmarks)
}).then(function () {
setStatus(reason || "已保存");
}).catch(function () {
setStatus("宿主保存失败,已本地兜底");
});
}
function openUrl(url) {
var finalUrl = normalizeUrl(url) || url;
if (hostReady) {
invoke("url.open", {
url: finalUrl,
closeAfterOpen: true
}).catch(function () {
location.href = finalUrl;
});
} else {
location.href = finalUrl;
}
}
function getFilteredData() {
var q = searchInput.value.trim().toLowerCase();
return bookmarks.filter(function (x) {
return !q ||
(x.name || "").toLowerCase().indexOf(q) >= 0 ||
(x.url || "").toLowerCase().indexOf(q) >= 0 ||
getHost(x.url).toLowerCase().indexOf(q) >= 0;
});
}
function findIndexById(id) {
for (var i = 0; i < bookmarks.length; i++) {
if (bookmarks[i].id === id) return i;
}
return -1;
}
function clearDropMark() {
var nodes = listEl.querySelectorAll(".dropTop,.dropBottom");
for (var i = 0; i < nodes.length; i++) {
nodes[i].classList.remove("dropTop");
nodes[i].classList.remove("dropBottom");
}
}
function moveBookmark(fromId, toId, pos) {
if (!fromId || !toId || fromId === toId) return false;
var fromIndex = findIndexById(fromId);
var toIndex = findIndexById(toId);
if (fromIndex < 0 || toIndex < 0) return false;
var item = bookmarks.splice(fromIndex, 1)[0];
toIndex = findIndexById(toId);
if (pos === "bottom") toIndex++;
if (toIndex < 0) toIndex = bookmarks.length;
if (toIndex > bookmarks.length) toIndex = bookmarks.length;
bookmarks.splice(toIndex, 0, item);
return true;
}
function render() {
var q = searchInput.value.trim();
var data = getFilteredData();
listEl.innerHTML = "";
clearDropMark();
countText.textContent = bookmarks.length + " 个";
if (!data.length) {
var empty = document.createElement("div");
empty.className = "empty";
empty.innerHTML = q
? "<div><b>没有匹配书签</b><br>回车可直接搜索当前关键词</div>"
: "<div><b>暂无书签</b><br>点击“添加”保存常用网址</div>";
listEl.appendChild(empty);
return;
}
data.forEach(function (item) {
var row = document.createElement("div");
row.className = "item";
row.setAttribute("draggable", "true");
row.setAttribute("data-id", item.id);
row.title = "点击打开:" + item.url;
if (item.id === dragId) row.classList.add("dragging");
var dragHandle = document.createElement("div");
dragHandle.className = "dragHandle";
dragHandle.textContent = "⋮⋮";
dragHandle.title = "拖动排序";
var icon = document.createElement("div");
icon.className = "favicon";
icon.textContent = getInitial(item.name, item.url);
var meta = document.createElement("div");
meta.className = "meta";
var name = document.createElement("div");
name.className = "name";
name.textContent = item.name || getHost(item.url);
var url = document.createElement("div");
url.className = "url";
url.textContent = item.url;
meta.appendChild(name);
meta.appendChild(url);
var editBtn = document.createElement("button");
editBtn.className = "iconBtn ghost";
editBtn.textContent = "✎";
editBtn.title = "修改名称 / 网址";
editBtn.onclick = function (e) {
e.stopPropagation();
beginEdit(item.id);
};
var delBtn = document.createElement("button");
delBtn.className = "iconBtn danger";
delBtn.textContent = "×";
delBtn.title = "删除";
delBtn.onclick = function (e) {
e.stopPropagation();
if (editingId === item.id) cancelEdit();
bookmarks = bookmarks.filter(function (x) {
return x.id !== item.id;
});
saveState("已删除");
render();
};
row.onclick = function () {
if (dragId) return;
openUrl(item.url);
};
row.ondragstart = function (e) {
dragId = item.id;
dropId = "";
dropPos = "";
closeAddLine();
cancelEdit();
try {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", item.id);
} catch (ex) {}
setTimeout(render, 0);
};
row.ondragover = function (e) {
if (!dragId || dragId === item.id) return;
e.preventDefault();
clearDropMark();
var rect = row.getBoundingClientRect();
var pos = e.clientY < rect.top + rect.height / 2 ? "top" : "bottom";
dropId = item.id;
dropPos = pos;
row.classList.add(pos === "top" ? "dropTop" : "dropBottom");
try {
e.dataTransfer.dropEffect = "move";
} catch (ex) {}
};
row.ondragleave = function () {
row.classList.remove("dropTop");
row.classList.remove("dropBottom");
};
row.ondrop = function (e) {
e.preventDefault();
if (moveBookmark(dragId, item.id, dropPos || "bottom")) {
saveState("排序已保存");
}
dragId = "";
dropId = "";
dropPos = "";
render();
};
row.ondragend = function () {
dragId = "";
dropId = "";
dropPos = "";
render();
};
row.appendChild(dragHandle);
row.appendChild(icon);
row.appendChild(meta);
row.appendChild(editBtn);
row.appendChild(delBtn);
listEl.appendChild(row);
});
}
function doSearch() {
var q = searchInput.value.trim();
if (!q) return;
var directUrl = normalizeUrl(q);
if (directUrl) {
openUrl(directUrl);
return;
}
var matched = bookmarks.filter(function (x) {
return (x.name || "").toLowerCase().indexOf(q.toLowerCase()) >= 0 ||
(x.url || "").toLowerCase().indexOf(q.toLowerCase()) >= 0;
});
if (matched.length === 1) {
openUrl(matched[0].url);
} else {
openUrl(searchUrl(q));
}
}
function closeAddLine() {
addLine.classList.remove("show");
toggleAddBtn.textContent = "添加";
}
function addBookmark() {
var rawUrl = urlInput.value.trim();
var url = normalizeUrl(rawUrl);
var name = nameInput.value.trim();
if (!url) {
if (normalizeUrl(searchInput.value.trim())) {
url = normalizeUrl(searchInput.value.trim());
} else {
setStatus("请输入有效网址");
urlInput.focus();
return;
}
}
if (!name) name = getHost(url);
var exists = false;
bookmarks = bookmarks.map(function (x) {
if ((x.url || "").toLowerCase() === url.toLowerCase()) {
exists = true;
return { id: x.id || makeId(), name: name, url: url };
}
return x;
});
if (!exists) {
bookmarks.unshift({ id: makeId(), name: name, url: url });
}
nameInput.value = "";
urlInput.value = "";
closeAddLine();
saveState(exists ? "已更新已有书签" : "已添加");
render();
}
function beginEdit(id) {
var index = findIndexById(id);
if (index < 0) return;
closeAddLine();
editingId = id;
editNameInput.value = bookmarks[index].name || getHost(bookmarks[index].url);
editUrlInput.value = bookmarks[index].url || "";
editPanel.classList.add("show");
editNameInput.focus();
editNameInput.select();
setStatus("正在编辑书签");
}
function cancelEdit() {
editingId = "";
editNameInput.value = "";
editUrlInput.value = "";
editPanel.classList.remove("show");
}
function saveEdit() {
var index = findIndexById(editingId);
if (index < 0) {
cancelEdit();
return;
}
var url = normalizeUrl(editUrlInput.value.trim());
var name = editNameInput.value.trim();
if (!url) {
setStatus("请输入有效网址");
editUrlInput.focus();
return;
}
if (!name) name = getHost(url);
var duplicated = false;
bookmarks = bookmarks.filter(function (x) {
if (x.id === editingId) return true;
if ((x.url || "").toLowerCase() === url.toLowerCase()) {
duplicated = true;
return false;
}
return true;
});
index = findIndexById(editingId);
if (index >= 0) {
bookmarks[index] = {
id: editingId,
name: name,
url: url
};
}
cancelEdit();
saveState(duplicated ? "已保存,并合并重复网址" : "修改已保存");
render();
}
function tryInitHost(retry) {
if (hasHost()) {
hostReady = true;
invoke("state.read", {
key: stateKey,
defaultValue: JSON.stringify(bookmarks)
}).then(function (res) {
if (typeof res === "string" && res) {
bookmarks = safeJsonParse(res, bookmarks);
} else if (res && typeof res.value === "string") {
bookmarks = safeJsonParse(res.value, bookmarks);
}
setStatus("宿主数据已同步");
render();
}).catch(function () {
setStatus("读取失败,使用本地内容");
render();
});
return;
}
if (retry < 30) {
setTimeout(function () {
tryInitHost(retry + 1);
}, 120);
} else {
setStatus("宿主未就绪,使用本地兜底");
}
}
searchInput.addEventListener("input", render);
searchInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") doSearch();
});
nameInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") addBookmark();
});
urlInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") addBookmark();
});
editNameInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") saveEdit();
});
editUrlInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") saveEdit();
});
searchBtn.onclick = doSearch;
saveBtn.onclick = addBookmark;
editSaveBtn.onclick = saveEdit;
editCancelBtn.onclick = function () {
cancelEdit();
setStatus("已取消编辑");
};
toggleAddBtn.onclick = function () {
cancelEdit();
var showing = addLine.classList.toggle("show");
toggleAddBtn.textContent = showing ? "收起" : "添加";
if (showing) {
var q = searchInput.value.trim();
var u = normalizeUrl(q);
if (u) {
urlInput.value = u;
nameInput.value = getHost(u);
nameInput.select();
nameInput.focus();
} else {
nameInput.focus();
}
}
};
loadFallback();
render();
tryInitHost(0);
})();
</script>
</body>
</html>
这个还缺少修改名称和拖动自由排序的功能
修改了。你右键选择修改其组件的提示词,然后发给ai。你完全可以自己修改,给deepseek或豆包或gpt。
我自己修改过了,这里提示一下,便于其他人用