// ocr.js
lucide.createIcons();
async function checkAuth() {
const r = await fetch('/api/auth/me');
const d = await r.json();
const profile = document.getElementById('userProfile');
const entry = document.getElementById('loginEntryBtn');
const submitBtn = document.getElementById('submitBtn');
if(d.logged_in) {
if(profile) profile.classList.remove('hidden');
if(entry) entry.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.classList.remove('opacity-50', 'cursor-not-allowed');
const phoneDisp = document.getElementById('userPhoneDisplay');
if(phoneDisp) phoneDisp.innerText = d.phone;
// 处理积分显示
const pointsBadge = document.getElementById('pointsBadge');
const pointsDisplay = document.getElementById('pointsDisplay');
if(pointsBadge && pointsDisplay) {
pointsBadge.classList.remove('hidden');
pointsDisplay.innerText = d.points;
}
const headerPoints = document.getElementById('headerPoints');
if(headerPoints) headerPoints.innerText = d.points;
} else {
if(profile) profile.classList.add('hidden');
if(entry) entry.classList.remove('hidden');
submitBtn.disabled = true;
submitBtn.classList.add('opacity-50', 'cursor-not-allowed');
}
lucide.createIcons();
}
let uploadedFiles = [];
const FIXED_MODEL = 'gemini-3-flash-preview';
function updateCostPreview() {
const costPreview = document.getElementById('costPreview');
const isPremium = document.getElementById('isPremium')?.checked || false;
// 默认消耗 1 积分,优质模式 X2
let cost = 1;
if(isPremium) cost *= 2;
costPreview.innerText = `本次解析将消耗 ${cost} 积分`;
costPreview.classList.remove('hidden');
}
function renderImagePreviews() {
const prev = document.getElementById('imagePreview');
if(!prev) return;
prev.innerHTML = '';
uploadedFiles.forEach((file, index) => {
const d = document.createElement('div');
d.className = 'relative w-24 h-24 rounded-2xl overflow-hidden flex-shrink-0 border-2 border-white shadow-md group';
d.innerHTML = `
图${index + 1}
`;
prev.appendChild(d);
const reader = new FileReader();
reader.onload = (ev) => {
d.innerHTML = `
图${index + 1}
`;
lucide.createIcons();
};
reader.readAsDataURL(file);
});
}
function removeUploadedFile(index) {
uploadedFiles.splice(index, 1);
renderImagePreviews();
}
function handleNewFiles(files) {
const newFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
if (newFiles.length === 0) return;
if (uploadedFiles.length + newFiles.length > 3) {
showToast('最多只能上传 3 张照片', 'warning');
const remaining = 3 - uploadedFiles.length;
if (remaining > 0) {
uploadedFiles = uploadedFiles.concat(newFiles.slice(0, remaining));
}
} else {
uploadedFiles = uploadedFiles.concat(newFiles);
}
renderImagePreviews();
}
document.getElementById('fileInput').onchange = (e) => {
handleNewFiles(e.target.files);
e.target.value = '';
};
// 粘贴上传逻辑
document.addEventListener('paste', (e) => {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
const files = [];
for (let item of items) {
if (item.type.indexOf('image') !== -1) {
files.push(item.getAsFile());
}
}
if (files.length > 0) {
handleNewFiles(files);
}
});
// === 高度定制的复制逻辑 ===
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 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;
}
}
}
const dropZone = document.getElementById('dropZone');
if (dropZone) {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(name => {
dropZone.addEventListener(name, (e) => {
e.preventDefault();
e.stopPropagation();
}, false);
});
dropZone.addEventListener('drop', (e) => {
handleNewFiles(e.dataTransfer.files);
}, false);
}
document.getElementById('isPremium').onchange = () => updateCostPreview();
document.getElementById('submitBtn').onclick = async () => {
const btn = document.getElementById('submitBtn');
const authCheck = await fetch('/api/auth/me');
const authData = await authCheck.json();
if(!authData.logged_in) return showToast('请先登录', 'warning');
if(uploadedFiles.length === 0) return showToast('请上传验光单照片', 'warning');
const isPremium = document.getElementById('isPremium')?.checked || false;
const cost = isPremium ? 2 : 1;
if(authData.points < cost) return showToast('积分不足', 'warning');
btn.disabled = true;
const btnText = btn.querySelector('span');
btnText.innerText = "AI 正在深度解读中...";
document.getElementById('statusInfo').classList.remove('hidden');
document.getElementById('placeholder').classList.add('hidden');
document.getElementById('finalWrapper').classList.remove('hidden');
const textResult = document.getElementById('textResult');
textResult.innerHTML = '正在提取光学数据...
';
lucide.createIcons();
try {
let image_urls = [];
const uploadData = new FormData();
for(let f of uploadedFiles) uploadData.append('images', f);
const upR = await fetch('/api/upload', { method: 'POST', body: uploadData });
const upRes = await upR.json();
if(upRes.error) throw new Error(upRes.error);
image_urls = upRes.urls;
const messages = [
{ role: "system", content: AppPrompts.systemMessage },
{
role: "user",
content: [
{ type: "text", text: "请解读这张验光单。" }
]
}
];
image_urls.forEach(url => {
messages[1].content.push({ type: "image_url", image_url: { url: url } });
});
const r = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mode: 'trial',
is_premium: isPremium,
prompt: "解读验光单",
model: FIXED_MODEL,
messages: messages,
image_urls: image_urls
})
});
const res = await r.json();
if(res.error) throw new Error(res.error);
const content = res.data[0].content;
// 分离正文和 JSON 数据
let displayMarkdown = content;
let jsonData = null;
const jsonMatch = content.match(/```json\n?([\s\S]*?)\n?```/);
if (jsonMatch && jsonMatch[1]) {
try {
jsonData = JSON.parse(jsonMatch[1].trim());
// 实时解析 Markdown (过滤 JSON 块)
displayMarkdown = content.replace(/```json[\s\S]*?```/g, '').trim();
} catch (e) {
console.error("JSON 解析失败", e);
}
}
// 渲染 Markdown
marked.setOptions({
gfm: true,
breaks: true,
tables: true,
headerIds: true,
mangle: false
});
textResult.innerHTML = marked.parse(displayMarkdown);
// 处理复制按钮逻辑
const actionBtn = document.getElementById('resultActions');
const copyBtn = document.getElementById('copyJsonBtn');
if (jsonData && actionBtn && copyBtn) {
actionBtn.classList.remove('hidden');
copyBtn.onclick = async () => {
const copyString = generateCopyString(jsonData);
const success = await copyToClipboard(copyString);
if (success) {
showToast('解析数据已成功复制', 'success');
} else {
showToast('复制失败,请手动选择', 'error');
}
};
}
checkAuth();
} catch (e) {
showToast('解读失败: ' + e.message, 'error');
textResult.innerHTML = `解读失败: ${e.message}
`;
} finally {
btn.disabled = false;
btnText.innerText = "开始解读验光单";
document.getElementById('statusInfo').classList.add('hidden');
}
};
async function init() {
checkAuth();
updateCostPreview();
}
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
if(!container) {
// 如果在 iframe 中,尝试调用父页面的 toast
if(window.parent && window.parent.showToast) {
window.parent.showToast(message, type);
return;
}
alert(message);
return;
}
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const colors = { success: 'text-emerald-500', error: 'text-rose-500', warning: 'text-amber-500', info: 'text-indigo-500' };
const icons = { success: 'check-circle', error: 'x-circle', warning: 'alert-triangle', info: 'info' };
toast.innerHTML = `${message}`;
container.appendChild(toast);
lucide.createIcons();
setTimeout(() => { toast.style.animation = 'slideOutRight 0.3s ease-in'; setTimeout(() => toast.remove(), 300); }, 3000);
}
init();