// ===== page-upload.jsx ===== const { useState: useSU, useEffect: useEU, useRef: useRU, useCallback: useCBU } = React; function UploadPage({ navigate }) { // States: idle → uploading → parsing → prescan → done → error const [stage, setStage] = useSU('idle'); const [file, setFile] = useSU(null); const [progress, setProgress] = useSU(0); const [dragging, setDragging] = useSU(false); const [orderData, setOrderData] = useSU(null); // 来自后端的真实订单 const [error, setError] = useSU(null); const meta = orderData && { name: orderData.file_name, sizeKb: file ? Math.round(file.size / 1024) : 0, words: orderData.word_count, estSec: 25, plan: orderData.plan === 'premium' ? `完整版 ¥${orderData.amount.toFixed(2)}` : `标准版 ¥${orderData.amount.toFixed(2)}`, orderId: orderData.order_id, amount: orderData.amount, }; // 真实上传 const beginUpload = useCBU(async (f) => { if (!f) return; setFile(f); setStage('uploading'); setProgress(0); setError(null); try { const data = await window.API.uploadOrder(f); window.API.saveToken(data.order_id, data.access_token); setOrderData(data); // 等进度条跑完再切到 parsing 视觉阶段 } catch (err) { console.error('upload failed', err); setError(err); setStage('error'); } }, []); // 合成上传进度(fetch 没有原生上传进度,做视觉效果) useEU(() => { if (stage !== 'uploading') return; const id = setInterval(() => { setProgress(p => { // 没收到响应时停在 80% 左右等待 const ceiling = orderData ? 100 : 85; if (p >= ceiling) return ceiling; return Math.min(ceiling, p + Math.random() * 12 + 4); }); }, 220); return () => clearInterval(id); }, [stage, orderData]); // progress 达到 100 后转 parsing useEU(() => { if (stage === 'uploading' && progress >= 100 && orderData) { const t = setTimeout(() => setStage('parsing'), 250); return () => clearTimeout(t); } }, [stage, progress, orderData]); useEU(() => { if (stage !== 'parsing') return; const t = setTimeout(() => setStage('prescan'), 1800); return () => clearTimeout(t); }, [stage]); const reset = () => { setStage('idle'); setFile(null); setProgress(0); setOrderData(null); setError(null); }; const goToOrder = () => { if (!orderData) return; // 把 access_token 一并写进 URL,使报告页可分享/可刷新/可换设备 navigate('order', { id: orderData.order_id, token: orderData.access_token }); }; return (
navigate('home')} className="hover:text-navy-900 cursor-pointer">首页 上传论文

上传你的论文

支持 .docx / .pdf / .txt 单文件 ≤ 10MB · 仅用于本次检测,24 小时内自动删除

{stage === 'idle' && ( )} {stage === 'uploading' && ( )} {stage === 'parsing' && ( )} {stage === 'prescan' && meta && ( setStage('done')} /> )} {stage === 'done' && meta && ( )} {stage === 'error' && ( )}
} title="端到端加密" desc="HTTPS 传输 + 服务端加密存储" /> } title="24h 自动删除" desc="完成后从服务器彻底清除" /> } title="不用于训练" desc="论文文本不进入模型训练集" />

文件检测完成后将在 24 小时内自动删除,不留底、不外传第三方。
检测结果仅供作者自查改进使用,不构成最终学术鉴定。

); } function DropZone({ dragging, setDragging, onPick }) { const fileRef = useRU(null); return (
{ e.preventDefault(); setDragging(true); }} onDragLeave={() => setDragging(false)} onDrop={(e) => { e.preventDefault(); setDragging(false); const f = e.dataTransfer.files?.[0]; onPick(f); }} onClick={() => fileRef.current?.click()} className={`relative cursor-pointer rounded-xl border-2 border-dashed transition-all p-12 text-center ${dragging ? 'border-brand-500 bg-brand-50' : 'border-ink-300 hover:border-brand-500 hover:bg-brand-50/40'}`} > onPick(e.target.files?.[0])} />
{dragging ? '松开即可上传' : '拖拽文件至此处 · 或 点击选择文件'}
.docx · .pdf · .txt  |  单文件 ≤ 10MB
首次免费试用前 1000 字
); } function UploadingPanel({ name, progress }) { return (
{name}
上传中 · {Math.round(progress)}%
{['上传文件', '解析文本', '准备检测'].map((s, i) => { const done = progress >= [33, 70, 100][i]; const active = !done && progress >= [0, 33, 70][i]; return (
{done ? : active ? : } {s}
); })}
); } function ParsingPanel({ name }) { const stages = [ { label: '提取文本内容', detail: '正在从 .docx 中抽取段落 ...', dur: 600 }, { label: '识别引用与脚注', detail: '排除参考文献部分,避免误判', dur: 500 }, { label: '初步统计建模', detail: '建立句长 / 句法骨架特征向量', dur: 700 }, ]; const [idx, setIdx] = useSU(0); useEU(() => { if (idx >= stages.length) return; const t = setTimeout(() => setIdx(i => i + 1), stages[idx].dur); return () => clearTimeout(t); }, [idx]); return (
{name}
正在解析文档结构 ...
检测引擎已就绪
{Array.from({ length: 9 }).map((_, i) => (
))}
PARSING · {Math.min(99, idx * 33 + 5)}%
    {stages.map((s, i) => { const done = i < idx; const active = i === idx; return (
  1. {done ? : active ? : }
    {s.label}
    {s.detail}
  2. ); })}
); } function PrescanPanel({ meta, onContinue }) { const previewParas = [ { risk: 'high', text: '在新时代的背景下,本文将从多个维度对该问题进行深入剖析,构建系统化的理论分析框架,为后续研究提供有益参考。' }, { risk: 'med', text: '研究表明,相关因素对于研究对象具有显著的促进作用。综上所述,本研究的开展具有重要意义。' }, { risk: 'high', text: '从总体上看,可以将其概括为三个方面,分别体现在认知、情感与行为三个层面,共同发挥着积极作用。' }, ]; return (
{meta.name}
解析完成 · {meta.words.toLocaleString()} 字 · {meta.sizeKb} KB
预扫描提示 · 初步识别
已识别到 3 段 疑似 AI 生成痕迹
付费后查看完整标记、模型指纹匹配与改写建议。预扫描仅基于句法模板初判,正式报告将启用全部 5 维度统计模型。
{previewParas.map((p, i) => { const tone = p.risk === 'high' ? { bg: '#FEF2F2', bar: '#EF4444', text: 'text-bad-600' } : { bg: '#FFFBEB', bar: '#F59E0B', text: 'text-warn-600' }; return (
§ {i + 1} {p.text}
); })}
… 还有 25 处 段落分析未展示 · 解锁完整报告即可查看
检测引擎已就绪 · 本次检测将与 8 大主流模型指纹比对 · 30 秒内生成完整报告
); } function FileInfoCard({ meta, onReplace, onPay }) { const [wordsAnim, setWordsAnim] = useSU(0); useEU(() => { let raf, start; const dur = 1400; const step = (ts) => { if (!start) start = ts; const t = Math.min(1, (ts - start) / dur); const e = 1 - Math.pow(1 - t, 3); setWordsAnim(Math.round(meta.words * e)); if (t < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); }, [meta.words]); return (
{meta.name}
上传成功 · 已通过完整性校验
{wordsAnim.toLocaleString()}} suffix="字" /> {meta.orderId}} /> ¥{meta.amount.toFixed(2)}} />
推荐方案 · 基于解析字数
{meta.plan}
您的论文为 {meta.words.toLocaleString()} 字,已自动匹配最合适的方案。
); } function ErrorPanel({ error, onRetry }) { const msg = error?.message || '上传失败,请重试'; return (

上传失败

{msg}

{error?.code &&

code: {error.code}

}
); } function Row({ label, value, suffix }) { return (
{label}
{value}{suffix && {suffix}}
); } function TipChip({ icon, title, desc }) { return (
{icon}
{title}
{desc}
); } window.UploadPage = UploadPage;