ai_v/main.js

415 lines
14 KiB
JavaScript
Raw Normal View History

// 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 = `<img src="${imgSrc}" class="max-w-full max-h-full object-contain">`;
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 = `<div class="p-4 bg-red-50 text-red-500 rounded-lg"><strong>出错了:</strong> ${err.message}</div>`;
if (els.loadingView) els.loadingView.classList.add('hidden');
els.resultContent.classList.remove('hidden');
} finally {
setLoading(false);
}
});