// ===== 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 (
上传你的论文
支持 .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 (
{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 (
-
{done ? : active ? : }
);
})}
);
}
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 (
);
})}
… 还有 25 处 段落分析未展示 · 解锁完整报告即可查看
检测引擎已就绪 · 本次检测将与 8 大主流模型指纹比对 · 30 秒内生成完整报告
} className="flex-1">
¥{meta.amount.toFixed(2)} 解锁完整报告
);
}
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()} 字,已自动匹配最合适的方案。
} className="sm:flex-shrink-0">重新上传
} className="flex-1">进入支付
);
}
function ErrorPanel({ error, onRetry }) {
const msg = error?.message || '上传失败,请重试';
return (
上传失败
{msg}
{error?.code &&
code: {error.code}
}
} className="mt-6">重新上传
);
}
function Row({ label, value, suffix }) {
return (
{label}
{value}{suffix && {suffix}}
);
}
function TipChip({ icon, title, desc }) {
return (
);
}
window.UploadPage = UploadPage;