Files
zk-data-agent/frontend/app/public/annotation-submissions.js
T
2026-07-20 17:28:47 +08:00

235 lines
8.4 KiB
JavaScript
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.
(() => {
const bridge = window.__annotationSubmissionBridge;
if (!bridge?.pageName || typeof bridge.read !== "function") return;
const endpoint = `/api/annotations/${encodeURIComponent(bridge.pageName)}/submissions`;
const toolbar = document.querySelector(".toolbar");
if (!toolbar) return;
const style = document.createElement("style");
style.textContent = `
.annotation-submit-status { color: #687385; font-size: 12px; white-space: nowrap; }
.annotation-history-dialog { width: min(680px, calc(100vw - 32px)); max-height: min(720px, calc(100vh - 32px)); padding: 0; border: 1px solid #d9dee7; border-radius: 8px; box-shadow: 0 24px 80px rgba(15, 23, 42, .2); }
.annotation-history-dialog::backdrop { background: rgba(15, 23, 42, .32); }
.annotation-history-head { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 18px; border-bottom: 1px solid #e5e9ef; background: #fff; }
.annotation-history-head h2 { margin: 0; font-size: 17px; }
.annotation-history-close { width: 32px; height: 32px; border: 0; border-radius: 5px; background: #f1f4f8; font-size: 20px; color: #556274; }
.annotation-history-list { margin: 0; padding: 0; list-style: none; overflow: auto; }
.annotation-history-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 14px 18px; border-bottom: 1px solid #edf0f4; }
.annotation-history-item:last-child { border-bottom: 0; }
.annotation-history-title { font-weight: 650; font-size: 14px; }
.annotation-history-meta { margin-top: 5px; color: #687385; font-size: 12px; }
.annotation-history-summary { margin-top: 7px; display: flex; flex-wrap: wrap; gap: 10px; color: #4b5666; font-size: 12px; }
.annotation-history-actions { display: flex; gap: 7px; }
.annotation-history-empty { padding: 40px 18px; text-align: center; color: #687385; }
@media (max-width: 820px) { .annotation-submit-status { display: none; } }
`;
document.head.appendChild(style);
const historyButton = button("提交记录", "secondary");
const submitButton = button("提交", "primary");
const status = document.createElement("span");
status.className = "annotation-submit-status";
toolbar.append(status, historyButton, submitButton);
const dialog = document.createElement("dialog");
dialog.className = "annotation-history-dialog";
dialog.innerHTML = `
<div class="annotation-history-head">
<h2>提交记录</h2>
<button class="annotation-history-close" aria-label="关闭">×</button>
</div>
<ol class="annotation-history-list"></ol>
`;
document.body.appendChild(dialog);
dialog.querySelector(".annotation-history-close").onclick = () =>
dialog.close();
dialog.addEventListener("click", (event) => {
if (event.target === dialog) dialog.close();
});
let history = [];
historyButton.onclick = async () => {
dialog.showModal();
await refreshHistory();
};
submitButton.onclick = async () => {
submitButton.disabled = true;
submitButton.textContent = "提交中";
try {
const response = await fetch(endpoint, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ annotations: bridge.read() }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "提交失败");
history = [
summaryOf(payload.submission),
...history.filter((item) => item.id !== payload.submission.id),
];
setStatus(payload.submission);
setRevisionUrl(payload.submission.id);
bridge.notify?.(`已提交第 ${payload.submission.version}`);
} catch (error) {
bridge.notify?.(error instanceof Error ? error.message : "提交失败");
} finally {
submitButton.disabled = false;
submitButton.textContent = "提交";
}
};
async function refreshHistory() {
const list = dialog.querySelector(".annotation-history-list");
list.innerHTML = '<li class="annotation-history-empty">正在读取</li>';
try {
const payload = await fetchJson(endpoint);
history = payload.history || [];
renderHistory();
} catch (error) {
list.innerHTML = `<li class="annotation-history-empty">${escapeHtml(error instanceof Error ? error.message : "读取失败")}</li>`;
}
}
function renderHistory() {
const list = dialog.querySelector(".annotation-history-list");
if (!history.length) {
list.innerHTML =
'<li class="annotation-history-empty">还没有提交记录</li>';
return;
}
list.innerHTML = "";
for (const item of history) {
const li = document.createElement("li");
li.className = "annotation-history-item";
li.innerHTML = `
<div>
<div class="annotation-history-title">第 ${item.version} 版 · ${escapeHtml(item.submittedBy?.username || "未知用户")}</div>
<div class="annotation-history-meta">${formatTime(item.submittedAt)}</div>
<div class="annotation-history-summary">
<span>已标 ${item.summary?.completed || 0}/${item.summary?.total || 0}</span>
<span>G ${item.summary?.G || 0}</span>
<span>S ${item.summary?.S || 0}</span>
<span>B ${item.summary?.B || 0}</span>
</div>
</div>
<div class="annotation-history-actions">
<button class="btn secondary" data-action="copy" type="button">复制链接</button>
<button class="btn secondary" data-action="view" type="button">查看</button>
</div>
`;
li.querySelector('[data-action="copy"]').onclick = async () => {
const url = revisionUrl(item.id);
try {
await navigator.clipboard.writeText(url);
bridge.notify?.("版本链接已复制");
} catch {
window.prompt("复制版本链接", url);
}
};
li.querySelector('[data-action="view"]').onclick = async () => {
try {
const payload = await fetchJson(
`${endpoint}?revision=${encodeURIComponent(item.id)}`,
);
bridge.apply(payload.submission.annotations || []);
dialog.close();
setStatus(payload.submission, "正在查看");
setRevisionUrl(payload.submission.id);
bridge.notify?.(`已打开第 ${payload.submission.version}`);
} catch (error) {
bridge.notify?.(error instanceof Error ? error.message : "读取失败");
}
};
list.appendChild(li);
}
}
async function initialize() {
try {
const requestedRevision = new URLSearchParams(window.location.search).get(
"revision",
);
if (requestedRevision) {
const payload = await fetchJson(
`${endpoint}?revision=${encodeURIComponent(requestedRevision)}`,
);
bridge.apply(payload.submission.annotations || []);
setStatus(payload.submission, "正在查看");
return;
}
const payload = await fetchJson(endpoint);
history = payload.history || [];
if (payload.latest) {
bridge.applyMissing(payload.latest.annotations || []);
setStatus(payload.latest);
} else {
status.textContent = "尚未提交";
}
} catch {
status.textContent = "提交记录暂不可用";
}
}
function setStatus(submission, prefix = "最新") {
status.textContent = `${prefix}:第 ${submission.version} 版 · ${submission.submittedBy?.username || "未知用户"}`;
}
function revisionUrl(revisionId) {
const url = new URL(window.location.href);
url.searchParams.set("revision", revisionId);
return url.toString();
}
function setRevisionUrl(revisionId) {
window.history.replaceState(null, "", revisionUrl(revisionId));
}
function button(label, variant) {
const element = document.createElement("button");
element.type = "button";
element.className = `btn ${variant}`;
element.textContent = label;
return element;
}
async function fetchJson(url) {
const response = await fetch(url, { cache: "no-store" });
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "请求失败");
return payload;
}
function summaryOf(submission) {
const { annotations: _annotations, ...summary } = submission;
return summary;
}
function formatTime(value) {
return new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
}).format(new Date(value));
}
function escapeHtml(value) {
return String(value ?? "").replace(
/[&<>"']/g,
(character) =>
({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
})[character],
);
}
void initialize();
})();