// ===== page-report.jsx ===== const { useState: useSR, useMemo: useMR, useRef: useRR, useEffect: useER } = React; const LEVEL_META = { low: { label: '低风险', desc: '文本呈现明显的人类写作特征,AI 生成概率较低。可放心提交。' }, mid: { label: '中风险', desc: '部分段落呈现可疑的 AI 生成痕迹,建议针对性核查并改写。' }, 'mid-high': { label: '中高风险', desc: '检测发现明显 AI 生成特征,建议对标记段落进行人工改写后再次自查。' }, high: { label: '高风险', desc: '文本呈现强烈的 AI 生成统计特征,建议对全文进行系统性人工改写。' }, }; function adaptApiReport(apiReport) { const mm = apiReport.model_match || {}; const models = [ { name: 'ChatGPT (GPT-4)', match: Math.round(mm.chatgpt || 0) }, { name: 'Claude', match: Math.round(mm.claude || 0) }, { name: 'DeepSeek', match: Math.round(mm.deepseek || 0) }, { name: '文心一言', match: Math.round(mm.wenxin || 0) }, { name: '通义千问', match: Math.round(mm.qwen || 0) }, { name: '豆包', match: Math.round(mm.doubao || 0) }, ].sort((a, b) => b.match - a.match); const meta = LEVEL_META[apiReport.risk_level] || LEVEL_META.mid; const dims = apiReport.dimensions || {}; const metaStats = apiReport.meta_stats || {}; return { id: apiReport.id, orderId: apiReport.order_id, title: (apiReport.file_name || '我的检测报告').replace(/\.[^.]+$/, ''), author: '私密用户', submitted: new Date(apiReport.created_at).toLocaleString('zh-CN'), words: apiReport.word_count || 0, durationSec: apiReport.duration_sec || 0, aigc: Math.round(apiReport.aigc_probability || 0), level: apiReport.risk_level, levelLabel: meta.label, levelDesc: meta.desc, summary: apiReport.summary || '', keyFindings: apiReport.key_findings || [], metaStats, clicheTop: metaStats.cliche_top || [], voiceTop: metaStats.voice_top || [], sampleInfo: metaStats.sample_info || {}, dims: { perplexity: Math.round(dims.perplexity || 0), burstiness: Math.round(dims.burstiness || 0), cliche: Math.round(dims.cliche || 0), regularity: Math.round(dims.regularity || 0), voice: Math.round(dims.voice || 0), }, models, paragraphs: apiReport.paragraphs || [], suggestions: apiReport.suggestions || [], }; } function ReportPage({ navigate, params }) { const requestedId = params?.id || 'r-1085'; const urlToken = params?.token || ''; const isDemo = requestedId.startsWith('r-'); // URL 自带 token 时立即落盘到 localStorage(覆盖旧值),保证后续刷新/再访问都能用 if (!isDemo && requestedId && urlToken) { window.API.saveToken(requestedId, urlToken); } const [reportId, setReportId] = useSR(requestedId); const [realReport, setRealReport] = useSR(null); const [loading, setLoading] = useSR(!isDemo); const [error, setError] = useSR(null); useER(() => { setReportId(requestedId); window.scrollTo({ top: 0, behavior: 'instant' }); if (requestedId.startsWith('r-')) { setRealReport(null); setLoading(false); setError(null); } else { setLoading(true); setError(null); // URL token 优先,否则 fall back 到 localStorage window.API.getReport(requestedId, urlToken || undefined) .then(data => setRealReport(adaptApiReport(data))) .catch(err => setError(err)) .finally(() => setLoading(false)); } }, [requestedId, urlToken]); // Demo mode allows switching between samples const handleSwitchSample = (id) => { if (!id.startsWith('r-')) return; setReportId(id); }; if (loading) { return (
加载报告中...
); } if (error) { const isNotReady = error.code === 'report_not_ready'; const isAuthFail = error.code === 'invalid_token' || error.status === 403; return (

{isNotReady ? '报告尚未生成' : isAuthFail ? '无权访问' : '报告加载失败'}

{isNotReady ? '请等待管理员确认收款,报告生成后页面会自动更新。' : isAuthFail ? '访问令牌已失效。如果你是订单本人,请通过订单页返回查看。' : (error.message || '未知错误')}

); } const report = isDemo ? (MOCK.REPORTS_BY_ID[reportId] || MOCK.REPORTS[1]) : realReport; if (!report) return null; const mountKey = `${reportId}`; const risk = getRisk(report.aigc); const pdfUrl = isDemo ? null : window.API.getReportPdfUrl(requestedId); return (
{/* 仅 demo 模式显示示例切换 */} {isDemo && (
示例样本 {MOCK.REPORTS.map(r => ( ))}
)}
AIGC 智能检测报告 · {(report.id || '').toUpperCase()}

{report.title}

} label="作者" value={report.author} /> } label="提交时间" value={report.submitted} /> } label="字数" value={`${(report.words || 0).toLocaleString()} 字`} /> {report.durationSec > 0 && } label="检测耗时" value={`${report.durationSec} 秒`} />} } label="引擎版本" value="Mojian v1.4" />
{pdfUrl ? ( ) : ( )}
风险等级
{report.levelLabel}

{report.levelDesc}

0 · 安全
30 · 低
60 · 中
100 · 高
p.risk !== 'safe').length} / ${report.paragraphs.length}`} tone="brand" />
{report.summary && ( <>

{report.summary}

)} {report.keyFindings && report.keyFindings.length > 0 && ( <>
    {report.keyFindings.map((f, i) => (
  • {String(i + 1).padStart(2, '0')}
    {f}
  • ))}
)} {(report.metaStats && report.metaStats.char_count) && ( <>
{(report.clicheTop.length > 0 || report.voiceTop.length > 0) && (
)} {report.sampleInfo && report.sampleInfo.strategy === 'smart_sample' && (
※ 全文较长(共 {report.sampleInfo.total} 段),段落级分析采用智能抽样(头尾 + 最长/最短 + 中间均匀采样)。 完整全文的 5 维统计指标基于全文真算(非抽样),上方数据为全文统计结果。
)}
)}
} />
最高匹配模型
{report.models[0]?.name || '-'}
匹配度 {report.models[0]?.match ?? 0}%

匹配度高仅表明文本与该模型生成样本在统计特征上接近,不构成"该模型生成"的判定。 用作改写参考即可。

模型指纹库
8 大主流模型 · 更新于 2026-05-18
    {report.suggestions.map((s, i) => (
  • {i + 1}
    {s}
  • ))} {report.suggestions.length === 0 && (
  • 本次检测未发现需要专项改写的高风险段落。
  • )}
改写小提示: 不要用 AI 反向"降 AIGC"——这往往会引入新的套话。更可靠的方法是替换为你自己的研究细节、反例和情绪化判断, 然后用墨鉴再次检测对照。
报告声明: 本报告基于多维度统计模型分析,结果仅供参考;检测算法持续迭代中,与各类官方查重系统的结果可能存在差异。 学术成果的最终判定请以所在机构的官方系统为准。 本平台不保留原文,所有上传文件在检测完成 24 小时内自动从服务器删除。
); } function MetaItem({ icon, label, value }) { return ( {icon} {label} {value} ); } function SectionTitle({ n, title, subtitle, extra }) { return (
{n} · SECTION

{title}

{subtitle &&

{subtitle}

}
{extra}
); } function MetricChip({ label, value, tone = 'ink' }) { const tones = { ink: 'bg-ink-50', ok: 'bg-ok-50 text-ok-600 ring-ok-100', brand: 'bg-brand-50 text-brand-600 ring-brand-100', }; return (
{label}
{value}
); } function getRisk(aigc) { if (aigc < 30) return { label: '低风险', color: '#059669', dotClass: 'bg-ok-500', tone: 'ok' }; if (aigc < 55) return { label: '中风险', color: '#D97706', dotClass: 'bg-warn-500', tone: 'warn' }; if (aigc < 75) return { label: '中高风险', color: '#EF4444', dotClass: 'bg-bad-500', tone: 'bad' }; return { label: '高风险', color: '#DC2626', dotClass: 'bg-bad-600', tone: 'bad' }; } function RiskIcon({ level }) { if (level === 'low') return
; if (level === 'mid') return
; return
; } function ReportHeader({ report, navigate, pdfUrl }) { return (
{(report.id || '').toUpperCase()} · 生成于 {report.submitted}
{pdfUrl ? ( ) : ( )}
); } function DimensionList({ dims }) { const [open, setOpen] = useSR(null); return (
{MOCK.DIMENSIONS.map(d => { const val = dims[d.key]; const isOpen = open === d.key; const tone = d.key === 'voice' ? (val >= 60 ? 'ok' : val >= 40 ? 'warn' : 'bad') : (val < 40 ? 'ok' : val < 65 ? 'warn' : 'bad'); const accent = tone === 'ok' ? '#10B981' : tone === 'warn' ? '#F59E0B' : '#EF4444'; return (
{d.desc}
); })}
); } function RiskLegend() { return (
{[ { color: '#10B981', bg: '#ECFDF5', label: '安全' }, { color: '#F59E0B', bg: '#FFFBEB', label: '中风险' }, { color: '#EF4444', bg: '#FEF2F2', label: '高风险' }, ].map(l => (
{l.label}
))}
); } function ParagraphSection({ paragraphs, metaStats }) { // 计数:用 prose_total(参与判定)而不是 total(含 skip) const dist = (metaStats && metaStats.paragraph_dist) || {}; const prose_total = dist.prose_total ?? paragraphs.filter(p => p.risk !== 'skip').length; const skipCount = dist.skip ?? paragraphs.filter(p => p.risk === 'skip').length; const high = dist.high ?? paragraphs.filter(p => p.risk === 'high').length; const med = dist.med ?? paragraphs.filter(p => p.risk === 'med').length; const safe = dist.safe ?? paragraphs.filter(p => p.risk === 'safe').length; // 过滤器:默认只看 med + high(信号最强的部分) const [filter, setFilter] = useSR('risky'); // 'risky' | 'high' | 'med' | 'safe' | 'all' | 'skip' const [renderLimit, setRenderLimit] = useSR(200); const filtered = useMR(() => { let arr = paragraphs; if (filter === 'risky') arr = paragraphs.filter(p => p.risk === 'med' || p.risk === 'high'); else if (filter === 'all') arr = paragraphs.filter(p => p.risk !== 'skip'); else arr = paragraphs.filter(p => p.risk === filter); return [...arr].sort((a, b) => (b.score ?? -1) - (a.score ?? -1)); }, [paragraphs, filter]); const shown = filtered.slice(0, renderLimit); const hiddenCount = filtered.length - shown.length; return (
{/* 风险分布概览 */}
{skipCount > 0 && (

※ 另有 {skipCount} 段为标题、列表项、代码段、参考条目等,由于样本特征不适合风险评分而被排除。

)} {/* 过滤器 */}
查看 {[ { k: 'risky', label: '中/高风险段', count: high + med, recommend: true }, { k: 'high', label: '仅高风险', count: high }, { k: 'med', label: '仅中风险', count: med }, { k: 'safe', label: '低风险段', count: safe }, { k: 'all', label: '全部论述段', count: prose_total }, ...(skipCount > 0 ? [{ k: 'skip', label: '非论述段', count: skipCount }] : []), ].map(opt => ( ))}
{/* 列表 */} {filtered.length === 0 ? (
无符合筛选条件的段落
{filter === 'risky' && '本地算法未在全文中发现中/高风险段。'}
) : (
{shown.map((p, i) => )} {hiddenCount > 0 && ( )}
)}
); } function DistChip({ count, total, label, tone }) { const tones = { bad: { bg: 'bg-bad-50', color: 'text-bad-600', bar: 'bg-bad-500' }, warn: { bg: 'bg-warn-50', color: 'text-warn-600', bar: 'bg-warn-500' }, ok: { bg: 'bg-ok-50', color: 'text-ok-600', bar: 'bg-ok-500' }, ink: { bg: 'bg-ink-50', color: 'text-ink-700', bar: 'bg-ink-400' }, }[tone] || { bg: 'bg-ink-50', color: 'text-ink-700', bar: 'bg-ink-400' }; const pct = total > 0 ? Math.round((count / total) * 100) : 0; return (
{count} / {total}
{label}
{pct}%
); } function ParagraphList({ paragraphs }) { // 兼容老用法(demo 模式直接传 paragraphs 数组) return (
{paragraphs.map((p, i) => )}
); } function Para({ para, idx }) { const [hover, setHover] = useSR(false); const TYPE_LABELS = { short: '标题/短段', list: '列表项', code: '代码/英文段', citation: '参考条目', prose: '论述段' }; const isSkip = para.risk === 'skip'; const tone = isSkip ? { bg: 'bg-ink-50', border: 'border-ink-200', bar: '#94A3B8', text: 'text-ink-500', label: TYPE_LABELS[para.type] || '非论述段', icon: } : para.risk === 'safe' ? { bg: 'bg-ok-50', border: 'border-ok-100', bar: '#10B981', text: 'text-ok-600', label: '低风险', icon: } : para.risk === 'med' ? { bg: 'bg-warn-50', border: 'border-warn-100', bar: '#F59E0B', text: 'text-warn-600', label: '中风险', icon: } : { bg: 'bg-bad-50', border: 'border-bad-100', bar: '#EF4444', text: 'text-bad-600', label: '高风险', icon: }; const tooShort = isSkip; const hasReason = para.llm_reviewed && para.reason; return (
setHover(true)} onMouseLeave={() => setHover(false)} className={`relative rounded-lg border ${tone.border} ${tone.bg} transition-shadow ${hover ? 'shadow-card' : ''} ${tooShort ? 'opacity-60' : ''}`}>
§{idx}

{para.text} {tooShort && (段落过短,未参与判定)}

{tone.icon}{tone.label} {!isSkip && para.score != null && ( AIGC 得分 {para.score} / 100 )} {isSkip && ( 不参与风险评分 )} {para.char_count != null && ( <> · {para.char_count} 字 )} {hasReason && ( <> · LLM 已点评 )}
{hasReason && (
LLM 点评
{para.reason}
)}
); } function StatBox({ label, value, unit, highlight }) { return (
{label}
{value}{unit && {unit}}
); } function HitsList({ title, tone, items, emptyText }) { const accent = tone === 'bad' ? 'text-bad-600 bg-bad-50' : 'text-ok-600 bg-ok-50'; return (

{title}

{items && items.length > 0 ? (
{items.map(([word, count], i) => (
{word} ×{count}
))}
) : (
{emptyText}
)}
); } window.ReportPage = ReportPage;