// main.js // 状态管理 let imageList = []; let finalMarkdown = ""; let extractedData = null; let statusInterval = null; // 用于动态文字切换 // DOM 元素引用 const els = { dropArea: document.getElementById('drop-area'), fileInput: document.getElementById('file-input'), placeholder: document.getElementById('upload-placeholder'), previewGrid: document.getElementById('preview-grid'), controlBar: document.getElementById('control-bar'), imgCount: document.getElementById('img-count'), clearBtn: document.getElementById('clear-btn'), analyzeBtn: document.getElementById('analyze-btn'), btnText: document.getElementById('btn-text'), btnSpinner: document.getElementById('btn-spinner'), // 结果区域视图 placeholderView: document.getElementById('placeholder-view'), // 初始欢迎页 loadingView: document.getElementById('loading-view'), // 加载动画页 loadingStatus: document.getElementById('loading-status-text'), // 动态提示文字 resultContent: document.getElementById('result-content'), // 结果显示页 resultScroll: document.getElementById('result-scroll'), // 滚动容器 copyBtn: document.getElementById('copy-btn') }; // === 事件监听 === els.dropArea.addEventListener('click', (e) => { if (e.target !== els.clearBtn && imageList.length < 2) els.fileInput.click(); }); els.fileInput.addEventListener('change', e => handleFiles(e.target.files)); document.addEventListener('paste', e => { const items = e.clipboardData.items; const files = []; for (let item of items) { if (item.type.indexOf('image') !== -1) files.push(item.getAsFile()); } if (files.length > 0) handleFiles(files); }); ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(name => { els.dropArea.addEventListener(name, e => { e.preventDefault(); e.stopPropagation(); }); }); els.dropArea.addEventListener('drop', e => handleFiles(e.dataTransfer.files)); els.clearBtn.addEventListener('click', (e) => { e.stopPropagation(); resetImages(); }); // === 复制按钮逻辑 === els.copyBtn.addEventListener('click', async () => { let textToCopy = ""; if (!extractedData) { textToCopy = els.resultContent.innerText; showCopyFeedback("⚠️ 未识别到数据,已复制全文"); } else { textToCopy = generateCopyString(extractedData); } const success = await copyToClipboard(textToCopy); if (success) { showCopyFeedback(extractedData ? "✅ 格式化数据已复制" : "✅ 已复制全文"); } else { prompt("浏览器禁止自动复制,请手动复制以下内容:", textToCopy); } }); // === 兼容性极强的复制函数 === async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); return true; } catch (err) { console.warn("现代复制API失败,尝试传统方法:", err); try { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.left = '-9999px'; textarea.style.top = '0'; textarea.setAttribute('readonly', ''); document.body.appendChild(textarea); textarea.focus(); textarea.select(); const successful = document.execCommand('copy'); document.body.removeChild(textarea); return successful; } catch (e) { return false; } } } function showCopyFeedback(msg) { const originalText = "📄 复制结果"; els.copyBtn.innerText = msg; setTimeout(() => { els.copyBtn.innerText = originalText; }, 2000); } // === 高度定制的复制逻辑 === function generateCopyString(data) { const r = data.right; const l = data.left; const tPd = data.total_pd; const blocks = []; const fmtDeg = (val) => { if (!val || val === "未标明" || val.trim() === "") return null; const num = parseFloat(val); if (isNaN(num)) return null; const deg = Math.round(num * 100); if (deg === 0) return null; if (deg < 0) return Math.abs(deg).toString(); if (deg > 0) return "+" + deg.toString(); return null; }; const fmtCyl = (val) => fmtDeg(val); const fmtRaw = (val) => (!val || val === "未标明" || val.trim() === "") ? null : val.trim(); const fmtPD = (val) => { if (!val || val === "未标明" || val.trim() === "") return null; const num = parseFloat(val); if (isNaN(num)) return val.trim(); return num.toString(); }; const rS = fmtDeg(r.s), lS = fmtDeg(l.s); const rC = fmtCyl(r.c), lC = fmtCyl(l.c); const rA = rC ? fmtRaw(r.a) : null, lA = lC ? fmtRaw(l.a) : null; const rAdd = fmtDeg(r.add), lAdd = fmtDeg(l.add); const rPH = fmtRaw(r.ph), lPH = fmtRaw(l.ph); const rPd = fmtPD(r.pd), lPd = fmtPD(l.pd), totalPd = fmtPD(tPd); if (!rS && !rC && !lS && !lC) return "平光"; const isDoubleSame = (rS === lS) && (rC === lC) && (rA === lA); if (isDoubleSame) { let block = `双眼${rS || ""}`; if (rC) block += `散光${rC}`; if (rA) block += `轴位${rA}`; blocks.push(block); } else { if (rS || rC) { let block = `右眼${rS || ""}`; if (rC) block += `散光${rC}`; if (rA) block += `轴位${rA}`; blocks.push(block); } if (lS || lC) { let block = `左眼${lS || ""}`; if (lC) block += `散光${lC}`; if (lA) block += `轴位${lA}`; blocks.push(block); } } if (rAdd && lAdd && rAdd === lAdd) blocks.push(`ADD${rAdd}`); else { if (rAdd) blocks.push(`ADD${rAdd}`); if (lAdd) blocks.push(`ADD${lAdd}`); } if (rPH) blocks.push(`瞳高右眼${rPH}`); if (lPH) blocks.push(`瞳高左眼${lPH}`); if (totalPd) blocks.push(`瞳距${totalPd}`); else { if (rPd) blocks.push(`右眼瞳距${rPd}`); if (lPd) blocks.push(`左眼瞳距${lPd}`); } return blocks.join(' '); } // === 核心逻辑函数 === async function handleFiles(files) { if (imageList.length >= 2) { alert("最多只能上传 2 张图片,请先清空后再试。"); return; } const remainingSlots = 2 - imageList.length; const filesProcess = Array.from(files).slice(0, remainingSlots); for (let file of filesProcess) { if (!file.type.startsWith('image/')) continue; try { const base64 = await readFileAsBase64(file); const compressed = await compressImage(base64, 1024, 0.8); imageList.push(compressed); } catch (err) { console.error("图片处理失败", err); } } updatePreviewUI(); } function resetImages() { imageList = []; updatePreviewUI(); els.fileInput.value = ''; } function updatePreviewUI() { const count = imageList.length; if (count === 0) { els.placeholder.classList.remove('hidden'); els.previewGrid.classList.add('hidden'); els.controlBar.classList.add('hidden'); } else { els.placeholder.classList.add('hidden'); els.previewGrid.classList.remove('hidden'); els.controlBar.classList.remove('hidden'); } els.previewGrid.innerHTML = ''; els.previewGrid.className = `w-full h-full grid gap-2 ${count > 1 ? 'grid-cols-2' : 'grid-cols-1'}`; imageList.forEach((imgSrc) => { const div = document.createElement('div'); div.className = "relative rounded-lg overflow-hidden border border-slate-200 bg-slate-100 flex items-center justify-center h-full max-h-[400px]"; div.innerHTML = ``; els.previewGrid.appendChild(div); }); els.imgCount.innerText = `${count}/2`; } function readFileAsBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = reject; reader.readAsDataURL(file); }); } function compressImage(base64Str, maxDim, quality) { return new Promise((resolve) => { const img = new Image(); img.src = base64Str; img.onload = () => { let w = img.width, h = img.height; if (w > maxDim || h > maxDim) { if (w > h) { h = Math.round(h * maxDim / w); w = maxDim; } else { w = Math.round(w * maxDim / h); h = maxDim; } } const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, w, h); resolve(canvas.toDataURL('image/jpeg', quality)); }; }); } // === 核心:设置加载状态 UI === function setLoading(isLoading) { els.analyzeBtn.disabled = isLoading; // 清除之前的定时器 if (statusInterval) { clearInterval(statusInterval); statusInterval = null; } if (isLoading) { els.btnText.innerText = "正在分析..."; els.btnSpinner.classList.remove('hidden'); if (els.placeholderView) els.placeholderView.classList.add('hidden'); if (els.resultContent) els.resultContent.classList.add('hidden'); if (els.loadingView) els.loadingView.classList.remove('hidden'); const statuses = [ "正在上传并扫描验光单...", "AI 正在识别视觉参数...", "正在校对球镜与柱镜数值...", "正在进行深度光学分析...", "正在生成专业解读报告...", "正在整理结果排版..." ]; let i = 0; if (els.loadingStatus) { els.loadingStatus.innerText = statuses[0]; statusInterval = setInterval(() => { if (i < statuses.length - 1) { i++; els.loadingStatus.innerText = statuses[i]; } else { // 播完了还没返回?进入“深度思考”省略号动画模式 clearInterval(statusInterval); let dots = 0; const baseText = statuses[statuses.length - 1]; statusInterval = setInterval(() => { dots = (dots + 1) % 4; els.loadingStatus.innerText = baseText + ".".repeat(dots); }, 500); } }, 3000); // 3秒切换一次,更真实 } } else { els.btnText.innerText = "开始智能解读"; els.btnSpinner.classList.add('hidden'); if (els.loadingView) els.loadingView.classList.add('hidden'); if (els.resultContent) els.resultContent.classList.remove('hidden'); } } // === API 调用逻辑 === els.analyzeBtn.addEventListener('click', async () => { if (imageList.length === 0) return alert('请至少上传 1 张验光单'); const apiKey = AppConfig.getDecryptedKey(); if (!apiKey) return alert("配置错误:无法读取 API Key"); setLoading(true); finalMarkdown = ""; extractedData = null; els.resultContent.innerHTML = ""; try { const userContent = AppPrompts.generateUserPayload(imageList); const response = await fetch(AppConfig.API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: AppConfig.MODEL, stream: true, messages: [ { role: "system", content: AppPrompts.systemMessage }, { role: "user", content: userContent } ] }) }); if (!response.ok) throw new Error(`API 请求失败: ${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); let hasStartedStreaming = false; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ') && line !== 'data: [DONE]') { try { const json = JSON.parse(line.substring(6)); const content = json.choices[0].delta?.content || ""; if (content && !hasStartedStreaming) { hasStartedStreaming = true; // 数据回来的一瞬间,立即停止加载动画和文字 if (els.loadingView) els.loadingView.classList.add('hidden'); els.resultContent.classList.remove('hidden'); if (statusInterval) clearInterval(statusInterval); } finalMarkdown += content; // 实时解析 Markdown (过滤 JSON 块) const displayMarkdown = finalMarkdown.replace(/```json[\s\S]*```/, ''); els.resultContent.innerHTML = marked.parse(displayMarkdown); // 自动滚动到底部 if (els.resultScroll) { els.resultScroll.scrollTop = els.resultScroll.scrollHeight; } } catch (e) {} } } } // 结束后提取 JSON 用于复制功能 const jsonMatch = finalMarkdown.match(/```json\n([\s\S]*?)\n```/); if (jsonMatch && jsonMatch[1]) { try { extractedData = JSON.parse(jsonMatch[1]); } catch (e) {} } els.copyBtn.disabled = false; els.copyBtn.classList.remove('opacity-50', 'cursor-not-allowed'); } catch (err) { els.resultContent.innerHTML = `
出错了: ${err.message}
`; if (els.loadingView) els.loadingView.classList.add('hidden'); els.resultContent.classList.remove('hidden'); } finally { setLoading(false); } });