// ============================================================================ // Vision Tool — 在线免费图片识别(原生单页) // ============================================================================ const $ = (id) => document.getElementById(id); // ---------- API 封装 ---------- async function api(path, options = {}) { const headers = options.headers || {}; if (!(options.body instanceof FormData)) { headers["Content-Type"] = "application/json"; } const token = localStorage.getItem("vt_token"); if (token) headers["Authorization"] = `Bearer ${token}`; const resp = await fetch(path, { ...options, headers }); let data; try { data = await resp.json(); } catch { throw new Error(`HTTP ${resp.status}: 响应解析失败`); } if (data.code !== 0) throw new Error(data.message || `HTTP ${resp.status}`); return data.data; } // ---------- 状态 ---------- let selectedFiles = []; // 待识别的图片文件 let analyzing = false; // ---------- 顶栏账号区 ---------- async function refreshAccount() { const area = $("accountArea"); area.innerHTML = ""; const { initialized } = await api("/api/v1/admin/check"); if (!initialized) { const btn = document.createElement("button"); btn.className = "btn btn-primary"; btn.textContent = "初始化管理员"; btn.onclick = () => showModal("initModal"); area.appendChild(btn); return; } const token = localStorage.getItem("vt_token"); if (!token) { const btn = document.createElement("button"); btn.className = "btn btn-ghost"; btn.textContent = "登录"; btn.onclick = () => showModal("loginModal"); area.appendChild(btn); return; } // 校验 token 有效性 try { const user = await api("/api/v1/user/profile"); const chip = document.createElement("span"); chip.className = "user-chip"; chip.textContent = `👤 ${user.username}`; area.appendChild(chip); const logout = document.createElement("button"); logout.className = "btn btn-ghost"; logout.textContent = "退出"; logout.onclick = () => { localStorage.removeItem("vt_token"); refreshAccount(); }; area.appendChild(logout); } catch { localStorage.removeItem("vt_token"); refreshAccount(); } } // ---------- 模态框 ---------- function showModal(id) { $(id).hidden = false; $("initError").textContent = ""; $("loginError").textContent = ""; } function hideModal(id) { $(id).hidden = true; } $("initModal").addEventListener("click", (e) => { if (e.target === e.currentTarget) hideModal("initModal"); }); $("loginModal").addEventListener("click", (e) => { if (e.target === e.currentTarget) hideModal("loginModal"); }); // 初始化管理员 $("initBtn").addEventListener("click", async () => { const username = $("initUsername").value.trim(); const password = $("initPassword").value; const password2 = $("initPassword2").value; if (!username || password.length < 6) { $("initError").textContent = "用户名不能为空,密码至少 6 位"; return; } if (password !== password2) { $("initError").textContent = "两次输入的密码不一致"; return; } try { await api("/api/v1/admin/init", { method: "POST", body: JSON.stringify({ username, password }), }); hideModal("initModal"); await login(username, password); refreshAccount(); } catch (err) { $("initError").textContent = err.message; } }); // 登录 $("loginBtn").addEventListener("click", async () => { const username = $("loginUsername").value.trim(); const password = $("loginPassword").value; if (!username || !password) { $("loginError").textContent = "请输入用户名和密码"; return; } try { await login(username, password); hideModal("loginModal"); refreshAccount(); } catch (err) { $("loginError").textContent = err.message; } }); async function login(username, password) { const data = await api("/api/v1/auth/login", { method: "POST", body: JSON.stringify({ username, password }), }); localStorage.setItem("vt_token", data.token); } // ---------- 图片选择与预览 ---------- const dropzone = $("dropzone"); const fileInput = $("fileInput"); dropzone.addEventListener("click", () => fileInput.click()); dropzone.addEventListener("dragover", (e) => { e.preventDefault(); dropzone.classList.add("dragover"); }); dropzone.addEventListener("dragleave", () => dropzone.classList.remove("dragover")); dropzone.addEventListener("drop", (e) => { e.preventDefault(); dropzone.classList.remove("dragover"); addFiles(e.dataTransfer.files); }); fileInput.addEventListener("change", () => { addFiles(fileInput.files); fileInput.value = ""; }); function addFiles(files) { for (const f of files) { if (!f.type.startsWith("image/")) continue; if (f.size > 10 * 1024 * 1024) { alert(`图片过大(>10MB): ${f.name}`); continue; } selectedFiles.push(f); } renderPreviews(); } function renderPreviews() { const list = $("previewList"); list.innerHTML = ""; selectedFiles.forEach((f, i) => { const item = document.createElement("div"); item.className = "preview-item"; const img = document.createElement("img"); img.src = URL.createObjectURL(f); img.alt = f.name; const rm = document.createElement("button"); rm.className = "remove"; rm.textContent = "×"; rm.onclick = () => { selectedFiles.splice(i, 1); renderPreviews(); }; item.append(img, rm); list.appendChild(item); }); $("analyzeBtn").disabled = selectedFiles.length === 0 || analyzing; } // ---------- 识别 ---------- $("analyzeBtn").addEventListener("click", analyze); async function analyze() { if (analyzing || selectedFiles.length === 0) return; analyzing = true; renderPreviews(); const btn = $("analyzeBtn"); btn.innerHTML = '识别中…'; const resultCard = $("resultCard"); resultCard.hidden = false; $("resultBody").textContent = "正在调用免费视觉模型,请稍候(最长约 2 分钟)…"; $("resultMeta").textContent = ""; const fd = new FormData(); selectedFiles.forEach((f) => fd.append("images", f, f.name)); const prompt = $("promptInput").value.trim(); if (prompt) fd.append("prompt", prompt); try { const data = await api("/api/v1/vision/analyze", { method: "POST", body: fd }); $("resultBody").textContent = data.result; if (data.usage) { $("resultMeta").textContent = `token 用量: prompt ${data.usage.prompt_tokens} / completion ${data.usage.completion_tokens} / total ${data.usage.total_tokens}`; } } catch (err) { $("resultBody").textContent = "❌ 识别失败: " + err.message; } finally { analyzing = false; btn.textContent = "开始识别"; renderPreviews(); } } // 复制结果 $("copyBtn").addEventListener("click", async () => { const text = $("resultBody").textContent; try { await navigator.clipboard.writeText(text); $("copyBtn").textContent = "已复制 ✓"; setTimeout(() => ($("copyBtn").textContent = "复制"), 1500); } catch { alert("复制失败,请手动选择文本"); } }); // ---------- 启动 ---------- refreshAccount().catch((err) => { // check 失败(服务未启动等),提示 document.querySelector(".brand p").textContent = "⚠️ 服务连接失败: " + err.message; });