// ===== page-order.jsx =====
const { useState: useSO, useEffect: useEO, useRef: useRO } = React;
// 后端 status → 前端 phase
const STATUS_TO_PHASE = {
pending: 'pending',
uploaded: 'uploaded',
processing: 'processing',
done: 'done',
refunded: 'refunded',
expired: 'expired',
llm_failed: 'failed',
};
function OrderPage({ navigate, params }) {
const orderId = params?.id || '';
const urlToken = params?.token || '';
// URL 自带 token 时立即落盘到 localStorage,避免后续丢失(关 tab/换设备等)
if (orderId && urlToken) {
window.API.saveToken(orderId, urlToken);
}
const [order, setOrder] = useSO(null);
const [loading, setLoading] = useSO(true);
const [error, setError] = useSO(null);
const [secondsLeft, setSecondsLeft] = useSO(0);
const [procProgress, setProcProgress] = useSO(0);
const phase = order ? (STATUS_TO_PHASE[order.status] || 'pending') : 'pending';
const pollingRef = useRO(null);
// 初次加载 + polling
const fetchOrder = async () => {
try {
const data = await window.API.getOrder(orderId);
setOrder(data);
setError(null);
} catch (err) {
console.error('fetch order failed', err);
setError(err);
} finally {
setLoading(false);
}
};
useEO(() => {
if (!orderId) {
setError({ message: '订单号缺失' });
setLoading(false);
return;
}
fetchOrder();
// 每 5s polling 一次
pollingRef.current = setInterval(fetchOrder, 5000);
return () => clearInterval(pollingRef.current);
}, [orderId]);
// 倒计时:基于 expires_at
useEO(() => {
if (!order || phase !== 'pending') return;
const tick = () => {
const expires = new Date(order.expires_at + (order.expires_at.endsWith('Z') ? '' : 'Z')).getTime();
const remaining = Math.max(0, Math.floor((expires - Date.now()) / 1000));
setSecondsLeft(remaining);
};
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [order, phase]);
// processing 阶段:合成进度条(仅视觉)
useEO(() => {
if (phase !== 'processing') { setProcProgress(0); return; }
let p = procProgress;
const id = setInterval(() => {
p = Math.min(95, p + 2 + Math.random() * 3);
setProcProgress(p);
}, 400);
return () => clearInterval(id);
}, [phase]);
// 完成时自动跳报告页
useEO(() => {
if (phase !== 'done' || !order) return;
setProcProgress(100);
const t = setTimeout(() => {
clearInterval(pollingRef.current);
// 把 token 一并带进报告页 URL(来源:URL query 或本地存储)
const t2 = urlToken || window.API.getToken(order.order_id);
navigate('report', { id: order.order_id, token: t2 });
}, 1200);
return () => clearTimeout(t);
}, [phase, order, navigate, urlToken]);
if (loading) {
return (
);
}
if (error || !order) {
return (
订单获取失败
{error?.message || '订单可能已过期或不存在'}
);
}
const amount = order.amount.toFixed(2);
const orderIdDisplay = `#${order.order_id}`;
const fmtTime = (s) => `${String(Math.floor(s/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`;
return (
AIGC 智能分析 · 5 维度统计模型
{order.word_count.toLocaleString()} 字
{order.plan === 'premium' ? '完整版' : '标准版'}
{order.file_name}
≤ 30 秒
应付金额
¥
{amount}
请按精确金额支付,便于系统自动对账。金额错误将无法自动识别订单。
{/* 保存访问凭证 */}
{/* 失败/退款/过期 状态 */}
{(phase === 'failed' || phase === 'refunded' || phase === 'expired') && (
{phase === 'failed' && '分析失败'}
{phase === 'refunded' && '订单已退款'}
{phase === 'expired' && '订单已过期'}
{phase === 'failed' && '检测引擎临时故障,已通知运维人工重试。如需立刻处理请联系 support@mojian.ai'}
{phase === 'refunded' && '订单金额已退回原支付账户,3 个工作日内到账。'}
{phase === 'expired' && '订单 30 分钟未支付,已自动关闭。可重新上传论文。'}
)}
🔥 本周已为
位同学完成检测 · 你不是一个人在熬夜
资金与隐私
- · 我们仅记录付款金额与订单号匹配,不获取你的支付宝账户信息
- · 30 分钟未支付订单自动关闭,论文文件 24 小时内同步删除
- · 异常订单可通过 support@mojian.ai 申请人工核对
);
}
function AccessCredsCard({ orderId }) {
const [copied, setCopied] = useSO('');
const token = window.API.getToken(orderId);
const url = `${window.location.origin}/#/order/${orderId}`;
if (!token) return null;
const copy = (text, key) => {
navigator.clipboard.writeText(text).then(() => {
setCopied(key);
setTimeout(() => setCopied(''), 1800);
}).catch(() => {});
};
return (
⚠ 保存你的访问凭证
如果你换设备或清缓存,需要凭这组凭证(订单号 + 访问令牌)才能找回报告。建议立刻复制下来。
订单号
{orderId}
访问令牌
{token}
完整访问链接
{url}
找回入口: window.location.hash = '#/lookup'}>底部"找回订单"
);
}
function KV({ label, children }) {
return (
{label}
{children}
);
}
function StatusTimeline({ phase, secondsLeft, fmtTime, procProgress }) {
const steps = [
{ key: 'pending', label: '等待支付', desc: '请在右侧完成支付宝扫码付款' },
{ key: 'uploaded', label: '已支付,等待确认', desc: '系统正在比对支付金额与订单' },
{ key: 'processing', label: '报告生成中', desc: 'LLM 多维度分析中,请稍候' },
{ key: 'done', label: '完成', desc: '报告已生成,即将跳转' },
];
const order = ['pending', 'uploaded', 'processing', 'done'];
const currentIdx = order.indexOf(phase);
return (
{steps.map((s, i) => {
const done = i < currentIdx;
const active = i === currentIdx;
return (
-
{done
?
: active
?
: }
{s.label}
{active && s.key === 'pending' && (
剩余 {fmtTime(secondsLeft)}
)}
{active && s.key === 'processing' && (
{Math.round(procProgress)}%
)}
{done &&
完成}
{s.desc}
{active && s.key === 'processing' && (
)}
);
})}
);
}
function FakeQR({ size = 220 }) {
const N = 25;
const cells = [];
let seed = 0x4d6f6a;
const rand = () => { seed = (seed * 9301 + 49297) % 233280; return seed / 233280; };
for (let r = 0; r < N; r++) for (let c = 0; c < N; c++) {
const inFinder = (r < 7 && c < 7) || (r < 7 && c >= N - 7) || (r >= N - 7 && c < 7);
if (inFinder) {
const lr = r < 7 ? r : r - (N - 7);
const lc = c < 7 ? c : c - (N - 7);
const onRing = lr === 0 || lr === 6 || lc === 0 || lc === 6;
const onCore = lr >= 2 && lr <= 4 && lc >= 2 && lc <= 4;
cells.push(onRing || onCore);
} else if (r === 6 || c === 6) {
cells.push((r + c) % 2 === 0);
} else {
cells.push(rand() > 0.52);
}
}
return (
);
}
// 真实支付宝码:若 /qrcode.png 存在则显示,否则降级为 FakeQR。
function PaymentQR({ size = 220 }) {
const [hasReal, setHasReal] = useSO(true);
if (!hasReal) return ;
return (
setHasReal(false)}
className="block rounded-md"
style={{ width: size, height: size, objectFit: 'contain' }}
/>
);
}
function PaymentPanel({ amount, suffix, phase, secondsLeft, fmtTime }) {
const paid = phase !== 'pending' && phase !== 'expired';
const suffixStr = String(suffix).padStart(2, '0');
return (
{paid ? '已收到支付' : '等待付款'}
请扫码支付
¥
{amount}
⚠ 金额必须精确为 ¥{amount},请勿修改默认金额
{phase === 'pending' && (
金额尾数 .{suffixStr} 是你的专属订单标识
{fmtTime(secondsLeft)}
超时后释放给他人 · 请勿关闭页面
)}
);
}
function CornerMarks() {
return (
<>
>
);
}
window.OrderPage = OrderPage;