管理台: - 内容管理新增名称搜索(title/slug/url/description),状态与翻页链接保留搜索词 - 菜单改为真实链接,每次点击重新向后台取数渲染分区,不再依赖手动刷新 - 内容管理每页固定 20 条 - pending 条目按钮改为「预览」,active 为「查看」 评论体系: - 评论提交新增必填联系方式(手机号/邮箱),前端不做展示 - 首次评论以「昵称+联系方式」建档,之后需匹配且未被禁用 - 后台评论管理新增联系方式列;新增评论人员管理页,可启用/禁用/改名/删除 - 评论昵称输入框不再预置 admin 相关项目: - 新增 entry_relations 双向关联,详情页侧栏展示并写入机读 JSON - 编辑页可添加/移除关联 内容源自动获取: - 修复 rss 适配器把 feed url 当成条目 url 的根本问题,支持 Atom link/href 与相对链接 - eryajf-weekly 限定正文容器取链接,修正项目说明的提取与「(opens new window)」清理 - pipeline 增加轮内去重与单条失败隔离;scheduler 对非法 cron 明确告警 MCP: - 重写 instructions 为八节说明,新增 usage 工具返回结构化接入文档 - 补全各工具的参数、用途与返回格式说明;baseUrl 取自当次请求 - 首页 MCP 区块与 README 同步补充写入示例与踩坑清单 测试: 新增 comments/relations/admin/ingest-adapters 用例,共 29 个全部通过
210 lines
7.2 KiB
JavaScript
210 lines
7.2 KiB
JavaScript
(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();
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll("[data-eye-toggle]").forEach(function (btn) {
|
||
btn.addEventListener("click", function () {
|
||
var input = btn.closest(".pwd-field, .ai-key-field")
|
||
? btn.closest(".pwd-field, .ai-key-field").querySelector("input")
|
||
: null;
|
||
if (!input) return;
|
||
var show = input.type === "password";
|
||
input.type = show ? "text" : "password";
|
||
btn.setAttribute("aria-label", show ? "隐藏密码" : "显示密码");
|
||
if (show) btn.setAttribute("data-show", "");
|
||
else btn.removeAttribute("data-show");
|
||
});
|
||
});
|
||
|
||
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,
|
||
remember: loginForm.remember ? loginForm.remember.checked : true,
|
||
})
|
||
.then(function () {
|
||
toast("登录成功,正在进入管理台…");
|
||
setTimeout(function () { location.href = "/admin"; }, 400);
|
||
})
|
||
.catch(function (err) { toast(err.message); });
|
||
});
|
||
}
|
||
|
||
var CONTACT_EMAIL = /^[^\s@]+@[^\s@]+\.[A-Za-z]{2,}$/;
|
||
var CONTACT_PHONE = /^1[3-9]\d{9}$/;
|
||
|
||
function validContact(v) {
|
||
var s = String(v == null ? "" : v).trim();
|
||
return CONTACT_EMAIL.test(s) || CONTACT_PHONE.test(s);
|
||
}
|
||
|
||
var commentForm = document.querySelector(".comment-form[data-entry-id]");
|
||
if (commentForm) {
|
||
var contactInput = commentForm.querySelector("[name=contact]");
|
||
if (contactInput) {
|
||
contactInput.addEventListener("input", function () {
|
||
if (contactInput.value.trim() && validContact(contactInput.value)) {
|
||
contactInput.setCustomValidity("");
|
||
} else {
|
||
contactInput.setCustomValidity("请填写正确的手机号或邮箱(不会公开显示)");
|
||
}
|
||
});
|
||
}
|
||
commentForm.addEventListener("submit", function (e) {
|
||
e.preventDefault();
|
||
var contact = contactInput ? contactInput.value.trim() : "";
|
||
if (!validContact(contact)) {
|
||
toast("请填写正确的联系方式:手机号或邮箱");
|
||
if (contactInput) contactInput.focus();
|
||
return;
|
||
}
|
||
if (!commentForm.reportValidity()) return;
|
||
postJson("/api/comments", {
|
||
entryId: Number(commentForm.getAttribute("data-entry-id")),
|
||
authorName: commentForm.querySelector("[name=authorName]").value,
|
||
contact: contact,
|
||
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); });
|
||
});
|
||
}
|
||
})();
|