feat: complete image console and add verified REST and MCP workflows
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
/* ==========================================================================
|
||||
components.js — 共享交互组件(toast / modal / 图标 / 格式化)
|
||||
========================================================================== */
|
||||
|
||||
/* ------------------------------------------------------------ 内联图标 */
|
||||
const ICONS = {
|
||||
images:
|
||||
'<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>',
|
||||
history:
|
||||
'<path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
|
||||
plug:
|
||||
'<path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v3a6 6 0 0 1-12 0V8Z"/>',
|
||||
boxes:
|
||||
'<path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z"/><path d="m7 16.5-4.74-2.85"/><path d="m7 16.5 5-3"/><path d="M7 16.5v5.17"/><path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z"/><path d="m17 16.5-5-3"/><path d="m17 16.5 4.74-2.85"/><path d="M17 16.5v5.17"/><path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z"/><path d="M12 8 7.26 5.15"/><path d="m12 8 4.74-2.85"/><path d="M12 13.5V8"/>',
|
||||
fileText:
|
||||
'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>',
|
||||
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
|
||||
trash:
|
||||
'<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/>',
|
||||
search:
|
||||
'<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
||||
refresh:
|
||||
'<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>',
|
||||
check: '<path d="M20 6 9 17l-5-5"/>',
|
||||
checkCircle:
|
||||
'<path d="M21.801 10A10 10 0 1 1 17 3.335"/><path d="m9 11 3 3L22 4"/>',
|
||||
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
||||
chevronLeft: '<path d="m15 18-6-6 6-6"/>',
|
||||
chevronRight: '<path d="m9 18 6-6-6-6"/>',
|
||||
chevronDown: '<path d="m6 9 6 6 6-6"/>',
|
||||
eye: '<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>',
|
||||
eyeOff:
|
||||
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.5 13.5 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/><path d="m2 2 20 20"/>',
|
||||
copy:
|
||||
'<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
|
||||
key: '<path d="m15.5 7.5 3 3L22 7l-3-3"/><path d="m21 2-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/>',
|
||||
alert:
|
||||
'<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
|
||||
info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
|
||||
loader: '<path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/>',
|
||||
download:
|
||||
'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
|
||||
externalLink:
|
||||
'<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
|
||||
image:
|
||||
'<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>',
|
||||
filter: '<path d="M3 4h18l-7 8v6l-4 2v-8Z"/>',
|
||||
code: '<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>',
|
||||
sparkles:
|
||||
'<path d="m12 3-1.9 5.8L4 10.7l6.1 1.9L12 18.4l1.9-5.8 6.1-1.9-6.1-1.9Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/>',
|
||||
database:
|
||||
'<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14a9 3 0 0 0 18 0V5"/><path d="M3 12a9 3 0 0 0 18 0"/>',
|
||||
clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
|
||||
zap: '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14Z"/>',
|
||||
settings:
|
||||
'<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2Z"/><circle cx="12" cy="12" r="3"/>',
|
||||
};
|
||||
|
||||
function icon(name, size = 16) {
|
||||
const body = ICONS[name] || ICONS.info;
|
||||
return `<svg class="ico" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- Toast */
|
||||
function toast(msg, type = "ok") {
|
||||
let host = document.getElementById("toast-host");
|
||||
if (!host) {
|
||||
host = document.createElement("div");
|
||||
host.id = "toast-host";
|
||||
document.body.appendChild(host);
|
||||
}
|
||||
const ic = type === "err" ? "alert" : type === "info" ? "info" : "checkCircle";
|
||||
const el = document.createElement("div");
|
||||
el.className = "toast" + (type === "err" ? " err" : type === "info" ? " info" : "");
|
||||
el.innerHTML = `${icon(ic, 16)}<span>${esc(msg)}</span>`;
|
||||
host.appendChild(el);
|
||||
setTimeout(() => {
|
||||
el.style.transition = "opacity .25s ease, transform .25s ease";
|
||||
el.style.opacity = "0";
|
||||
el.style.transform = "translateY(6px)";
|
||||
setTimeout(() => el.remove(), 260);
|
||||
}, 2600);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- Modal */
|
||||
function openModal(id) {
|
||||
const m = document.getElementById(id);
|
||||
if (m) m.classList.add("open");
|
||||
}
|
||||
function closeModal(id) {
|
||||
const m = document.getElementById(id);
|
||||
if (m) m.classList.remove("open");
|
||||
}
|
||||
|
||||
function paintIcons(root = document) {
|
||||
root.querySelectorAll("[data-icon]").forEach((el) => {
|
||||
el.outerHTML = icon(el.getAttribute("data-icon"), 16);
|
||||
});
|
||||
}
|
||||
|
||||
async function runTask(task, button) {
|
||||
if (button && button.disabled) return;
|
||||
if (button) { button.disabled = true; button.classList.add("is-loading"); }
|
||||
try {
|
||||
return await task();
|
||||
} catch (err) {
|
||||
toast(err.message || "操作失败,请重试", "err");
|
||||
} finally {
|
||||
if (button) { button.disabled = false; button.classList.remove("is-loading"); }
|
||||
}
|
||||
}
|
||||
document.addEventListener("click", (e) => {
|
||||
if (e.target.classList && e.target.classList.contains("modal-mask")) {
|
||||
e.target.classList.remove("open");
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
document.querySelectorAll(".modal-mask.open").forEach((m) => m.classList.remove("open"));
|
||||
document.querySelectorAll(".lightbox.open").forEach((m) => m.classList.remove("open"));
|
||||
}
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------ 格式化 */
|
||||
function fmtBytes(n) {
|
||||
if (!n && n !== 0) return "—";
|
||||
if (n < 1024) return n + " B";
|
||||
if (n < 1048576) return (n / 1024).toFixed(1) + " KB";
|
||||
return (n / 1048576).toFixed(2) + " MB";
|
||||
}
|
||||
function fmtDuration(ms) {
|
||||
if (ms == null) return "—";
|
||||
return ms >= 1000 ? (ms / 1000).toFixed(1) + " s" : ms + " ms";
|
||||
}
|
||||
function esc(s) {
|
||||
return String(s == null ? "" : s).replace(/[&<>"']/g, (c) =>
|
||||
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])
|
||||
);
|
||||
}
|
||||
function copyText(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => toast("已复制到剪贴板"),
|
||||
() => toast("复制失败,请手动选择", "err")
|
||||
);
|
||||
return;
|
||||
}
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
toast("已复制到剪贴板");
|
||||
} catch (err) {
|
||||
toast("复制失败,请手动选择", "err");
|
||||
}
|
||||
ta.remove();
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- Markdown 极简渲染 */
|
||||
function renderMarkdown(md) {
|
||||
const lines = String(md).split("\n");
|
||||
let out = "";
|
||||
let inCode = false;
|
||||
let inList = false;
|
||||
let inTable = false;
|
||||
|
||||
const inline = (s) =>
|
||||
esc(s)
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/(^|[^*])\*([^*]+)\*/g, "$1<em>$2</em>")
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
||||
|
||||
const closeList = () => { if (inList) { out += "</ul>"; inList = false; } };
|
||||
const closeTable = () => { if (inTable) { out += "</tbody></table>"; inTable = false; } };
|
||||
|
||||
for (const raw of lines) {
|
||||
const line = raw.replace(/\s+$/, "");
|
||||
if (line.trim().startsWith("```")) {
|
||||
closeList(); closeTable();
|
||||
out += inCode ? "</code></pre>" : "<pre><code>";
|
||||
inCode = !inCode;
|
||||
continue;
|
||||
}
|
||||
if (inCode) { out += esc(raw) + "\n"; continue; }
|
||||
|
||||
if (!line.trim()) { closeList(); closeTable(); continue; }
|
||||
|
||||
const h = line.match(/^(#{1,4})\s+(.*)$/);
|
||||
if (h) {
|
||||
closeList(); closeTable();
|
||||
const lv = h[1].length;
|
||||
out += `<h${lv}>${inline(h[2])}</h${lv}>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^\s*\|/.test(line)) {
|
||||
const cells = line.split("|").slice(1, -1).map((c) => c.trim());
|
||||
if (/^[\s|:-]+$/.test(line)) continue;
|
||||
if (!inTable) {
|
||||
closeList();
|
||||
out += "<table><thead><tr>" + cells.map((c) => `<th>${inline(c)}</th>`).join("") + "</tr></thead><tbody>";
|
||||
inTable = true;
|
||||
} else {
|
||||
out += "<tr>" + cells.map((c) => `<td>${inline(c)}</td>`).join("") + "</tr>";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
closeTable();
|
||||
|
||||
const li = line.match(/^\s*[-*]\s+(.*)$/);
|
||||
if (li) {
|
||||
if (!inList) { out += "<ul>"; inList = true; }
|
||||
out += `<li>${inline(li[1])}</li>`;
|
||||
continue;
|
||||
}
|
||||
closeList();
|
||||
|
||||
const bq = line.match(/^>\s?(.*)$/);
|
||||
if (bq) { out += `<blockquote>${inline(bq[1])}</blockquote>`; continue; }
|
||||
|
||||
out += `<p>${inline(line)}</p>`;
|
||||
}
|
||||
closeList(); closeTable();
|
||||
if (inCode) out += "</code></pre>";
|
||||
return out;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- 激活导航项(CSS 兜底) */
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const page = document.body.getAttribute("data-page");
|
||||
if (!page) return;
|
||||
const item = document.querySelector(`.app-nav a[data-nav="${page}"]`);
|
||||
if (item) item.setAttribute("aria-current", "page");
|
||||
});
|
||||
Reference in New Issue
Block a user