(() => {
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 = `
提交记录
`;
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 = '正在读取';
try {
const payload = await fetchJson(endpoint);
history = payload.history || [];
renderHistory();
} catch (error) {
list.innerHTML = `${escapeHtml(error instanceof Error ? error.message : "读取失败")}`;
}
}
function renderHistory() {
const list = dialog.querySelector(".annotation-history-list");
if (!history.length) {
list.innerHTML =
'还没有提交记录';
return;
}
list.innerHTML = "";
for (const item of history) {
const li = document.createElement("li");
li.className = "annotation-history-item";
li.innerHTML = `
第 ${item.version} 版 · ${escapeHtml(item.submittedBy?.username || "未知用户")}
${formatTime(item.submittedAt)}
已标 ${item.summary?.completed || 0}/${item.summary?.total || 0}
G ${item.summary?.G || 0}
S ${item.summary?.S || 0}
B ${item.summary?.B || 0}
`;
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) =>
({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
})[character],
);
}
void initialize();
})();