feat: awesome index 全功能实现

- M1-M2: 数据层与核心API(DuckDB, entries/tags/comments/search服务)
- M3: SSR页面(EJS模板,接真实数据)
- M4: 管理后台(登录/用户/内容源/AI ingest)
- M5: 采集层(scheduler+rss/html/eryajf适配器)
- M6: MCP只读接口(Streamable HTTP, 4工具)
- M7: Docker部署(Dockerfile+docker-compose)
- 测试: 10个用例全部通过
- 修复: DuckDB连接参数绑定、状态机迁移、标签树操作
This commit is contained in:
cheney
2026-08-28 09:32:48 +08:00
parent c47bb15d8c
commit dec103546e
51 changed files with 7298 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
(function () {
"use strict";
var toastEl = document.querySelector(".toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
window.awToast = toast;
async function postJson(url, body, method) {
var res = await fetch(url, {
method: method || "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body || {}),
});
var json = null;
try {
json = await res.json();
} catch (e) {}
if (!res.ok || (json && json.ok === false)) {
throw new Error((json && json.error && json.error.message) || "请求失败(" + res.status + ")");
}
return json;
}
window.awApi = postJson;
document.querySelectorAll("[data-copy]").forEach(function (btn) {
btn.addEventListener("click", function () {
var text = btn.getAttribute("data-copy");
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(
function () { toast("已复制"); },
function () { toast("复制失败,请手动选择文本"); }
);
} else {
toast("当前环境不支持自动复制");
}
});
});
var searchInput = document.getElementById("q");
var echo = document.getElementById("query-echo");
if (searchInput && echo) {
var paint = function () {
var q = searchInput.value.trim();
echo.textContent = q
? '→ mcp: search_entries({ "q": "' + q + '", "limit": 20 })'
: '→ mcp: search_entries({ "q": "…" })';
};
searchInput.addEventListener("input", paint);
paint();
}
var searchForm = document.getElementById("search-form");
if (searchForm) {
searchForm.addEventListener("submit", function (e) {
e.preventDefault();
var q = searchForm.querySelector("#q") ? searchForm.querySelector("#q").value.trim() : "";
location.href = "/entries" + (q ? "?q=" + encodeURIComponent(q) : "");
});
}
var randomBtn = document.getElementById("random-btn");
if (randomBtn) {
randomBtn.addEventListener("click", function () {
location.href = "/random";
});
}
var navToggle = document.querySelector(".nav-toggle");
if (navToggle) {
navToggle.addEventListener("click", function () {
var bar = document.querySelector(".topbar");
var open = bar.classList.toggle("menu-open");
navToggle.setAttribute("aria-expanded", open ? "true" : "false");
});
}
var railToggle = document.getElementById("rail-toggle");
if (railToggle) {
railToggle.addEventListener("click", function () {
document.getElementById("filter-rail").classList.toggle("open");
});
}
document.querySelectorAll(".tree-row[data-tree]").forEach(function (row) {
row.addEventListener("click", function () {
var target = document.getElementById(row.getAttribute("data-tree"));
if (!target) return;
var open = target.classList.toggle("open");
row.setAttribute("aria-expanded", open ? "true" : "false");
});
});
document.querySelectorAll(".view-tabs button[data-view]").forEach(function (tab) {
tab.addEventListener("click", function () {
document
.querySelectorAll(".view-tabs button[data-view]")
.forEach(function (t) { t.setAttribute("aria-selected", "false"); });
tab.setAttribute("aria-selected", "true");
var view = tab.getAttribute("data-view");
document.querySelectorAll("[data-view-panel]").forEach(function (p) {
p.hidden = p.getAttribute("data-view-panel") !== view;
});
});
});
document.querySelectorAll(".filter-auto input[type=checkbox], .filter-auto select").forEach(function (el) {
el.addEventListener("change", function () {
el.form && el.form.submit();
});
});
var loginForm = document.getElementById("login-form");
if (loginForm) {
loginForm.addEventListener("submit", function (e) {
e.preventDefault();
if (!loginForm.reportValidity()) return;
postJson("/api/admin/login", {
username: loginForm.username.value,
password: loginForm.password.value,
})
.then(function () {
toast("登录成功,正在进入管理台…");
setTimeout(function () { location.href = "/admin"; }, 400);
})
.catch(function (err) { toast(err.message); });
});
}
var commentForm = document.querySelector(".comment-form[data-entry-id]");
if (commentForm) {
commentForm.addEventListener("submit", function (e) {
e.preventDefault();
if (!commentForm.reportValidity()) return;
postJson("/api/comments", {
entryId: Number(commentForm.getAttribute("data-entry-id")),
authorName: commentForm.querySelector("[name=authorName]").value,
body: commentForm.querySelector("[name=body]").value,
})
.then(function () {
toast("评论已发布");
setTimeout(function () { location.reload(); }, 500);
})
.catch(function (err) { toast(err.message); });
});
}
var feedbackForm = document.querySelector(".feedback[data-feedback]");
if (feedbackForm) {
feedbackForm.addEventListener("submit", function (e) {
e.preventDefault();
if (!feedbackForm.reportValidity()) return;
postJson("/api/feedback", { body: feedbackForm.querySelector("textarea").value })
.then(function () {
toast("已收到,感谢反馈");
feedbackForm.reset();
})
.catch(function (err) { toast(err.message); });
});
}
})();