feat: 首页增加工具箱功能区、剧本评测可视化展示;重构剧本评分页面UI #6
@@ -46,16 +46,126 @@ function getGrade(score: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const HISTORY_KEY = "omniai:script-eval-history";
|
const HISTORY_KEY = "omniai:script-eval-history";
|
||||||
|
const TEXT_FILE_EXTENSIONS = [
|
||||||
|
".txt",
|
||||||
|
".text",
|
||||||
|
".md",
|
||||||
|
".markdown",
|
||||||
|
".fountain",
|
||||||
|
".fdx",
|
||||||
|
".rtf",
|
||||||
|
".csv",
|
||||||
|
".tsv",
|
||||||
|
".json",
|
||||||
|
".jsonl",
|
||||||
|
".xml",
|
||||||
|
".html",
|
||||||
|
".htm",
|
||||||
|
".yaml",
|
||||||
|
".yml",
|
||||||
|
".toml",
|
||||||
|
".ini",
|
||||||
|
".conf",
|
||||||
|
".cfg",
|
||||||
|
".properties",
|
||||||
|
".log",
|
||||||
|
".srt",
|
||||||
|
".ass",
|
||||||
|
".ssa",
|
||||||
|
".vtt",
|
||||||
|
".sql",
|
||||||
|
".js",
|
||||||
|
".jsx",
|
||||||
|
".ts",
|
||||||
|
".tsx",
|
||||||
|
".py",
|
||||||
|
".java",
|
||||||
|
".c",
|
||||||
|
".cpp",
|
||||||
|
".h",
|
||||||
|
".hpp",
|
||||||
|
".cs",
|
||||||
|
".go",
|
||||||
|
".rs",
|
||||||
|
".php",
|
||||||
|
".rb",
|
||||||
|
".sh",
|
||||||
|
".bat",
|
||||||
|
".ps1",
|
||||||
|
".lua",
|
||||||
|
".swift",
|
||||||
|
".kt",
|
||||||
|
".kts",
|
||||||
|
] as const;
|
||||||
|
const TEXT_FILE_EXTENSION_SET = new Set<string>(TEXT_FILE_EXTENSIONS);
|
||||||
|
const TEXT_FILE_ACCEPT = TEXT_FILE_EXTENSIONS.join(",");
|
||||||
|
const TEXT_FILE_HINT = "支持常见文本格式:TXT / MD / Fountain / FDX / RTF / JSON / CSV / XML / HTML / YAML / LOG / 字幕等";
|
||||||
|
|
||||||
function loadHistory(): HistoryEntry[] {
|
function loadHistory(): HistoryEntry[] {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(HISTORY_KEY);
|
const raw = localStorage.getItem(HISTORY_KEY);
|
||||||
return raw ? (JSON.parse(raw) as HistoryEntry[]) : [];
|
return raw ? (JSON.parse(raw) as HistoryEntry[]).sort((a, b) => b.timestamp - a.timestamp) : [];
|
||||||
} catch { return []; }
|
} catch { return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveHistory(entries: HistoryEntry[]) {
|
function saveHistory(entries: HistoryEntry[]) {
|
||||||
try { localStorage.setItem(HISTORY_KEY, JSON.stringify(entries.slice(0, 20))); } catch { /* quota exceeded */ }
|
try { localStorage.setItem(HISTORY_KEY, JSON.stringify(entries.sort((a, b) => b.timestamp - a.timestamp).slice(0, 20))); } catch { /* quota exceeded */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileExtension(filename: string): string {
|
||||||
|
const dotIndex = filename.lastIndexOf(".");
|
||||||
|
return dotIndex >= 0 ? filename.slice(dotIndex).toLowerCase() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isReadableTextFile(file: File, ext: string): boolean {
|
||||||
|
const mime = file.type.toLowerCase();
|
||||||
|
return (
|
||||||
|
TEXT_FILE_EXTENSION_SET.has(ext) ||
|
||||||
|
mime.startsWith("text/") ||
|
||||||
|
mime === "application/json" ||
|
||||||
|
mime === "application/xml" ||
|
||||||
|
mime === "application/xhtml+xml" ||
|
||||||
|
mime === "application/x-yaml" ||
|
||||||
|
mime === "application/yaml"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decodeTextFile(file: File): Promise<string> {
|
||||||
|
const bytes = await file.arrayBuffer();
|
||||||
|
const utf8 = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
||||||
|
if (!utf8.includes("\uFFFD")) return utf8;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new TextDecoder("gb18030", { fatal: false }).decode(bytes);
|
||||||
|
} catch {
|
||||||
|
return utf8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUploadedText(raw: string, ext: string): string {
|
||||||
|
if (ext === ".rtf") {
|
||||||
|
const text = raw
|
||||||
|
.replace(/\\par[d]?/gi, "\n")
|
||||||
|
.replace(/\\line/gi, "\n")
|
||||||
|
.replace(/\\'[0-9a-f]{2}/gi, "")
|
||||||
|
.replace(/\\[a-z]+\d* ?/gi, "")
|
||||||
|
.replace(/[{}]/g, "")
|
||||||
|
.replace(/\n{3,}/g, "\n\n")
|
||||||
|
.trim();
|
||||||
|
return text || raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([".html", ".htm", ".xml", ".fdx"].includes(ext) && typeof DOMParser !== "undefined") {
|
||||||
|
try {
|
||||||
|
const doc = new DOMParser().parseFromString(raw, ext === ".html" || ext === ".htm" ? "text/html" : "application/xml");
|
||||||
|
const text = doc.documentElement.textContent?.replace(/\n{3,}/g, "\n\n").trim();
|
||||||
|
return text || raw;
|
||||||
|
} catch {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCORE_DIMENSIONS: ScoreDimension[] = [
|
const SCORE_DIMENSIONS: ScoreDimension[] = [
|
||||||
@@ -138,13 +248,14 @@ function ScriptTokensPage() {
|
|||||||
const handleFileUpload = async (event: ChangeEvent<HTMLInputElement>) => {
|
const handleFileUpload = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const ext = file.name.slice(file.name.lastIndexOf(".")).toLowerCase();
|
const ext = getFileExtension(file.name);
|
||||||
const readable = [".txt", ".md"].includes(ext) || file.type === "text/plain" || file.type === "text/markdown";
|
const readable = isReadableTextFile(file, ext);
|
||||||
setUploadedFile({ name: file.name, size: file.size });
|
setUploadedFile({ name: file.name, size: file.size });
|
||||||
if (readable) {
|
if (readable) {
|
||||||
setScript(await file.text());
|
const text = normalizeUploadedText(await decodeTextFile(file), ext);
|
||||||
|
setScript(text);
|
||||||
} else {
|
} else {
|
||||||
setScript(`[已上传文件:${file.name}]\n\n暂不支持解析 ${ext.toUpperCase()} 格式,请上传 TXT 或 MD 文件。`);
|
setScript(`[已上传文件:${file.name}]\n\n暂不支持解析 ${ext ? ext.toUpperCase() : "未知"} 格式,请上传常见文本类文件。`);
|
||||||
}
|
}
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
};
|
};
|
||||||
@@ -167,7 +278,9 @@ function ScriptTokensPage() {
|
|||||||
score: aiResult.totalScore,
|
score: aiResult.totalScore,
|
||||||
grade: g,
|
grade: g,
|
||||||
};
|
};
|
||||||
const updated = [entry, ...loadHistory().filter((h) => h.name !== entry.name || h.score !== entry.score)];
|
const updated = [entry, ...loadHistory().filter((h) => h.name !== entry.name || h.score !== entry.score)].sort(
|
||||||
|
(a, b) => b.timestamp - a.timestamp,
|
||||||
|
);
|
||||||
saveHistory(updated);
|
saveHistory(updated);
|
||||||
setHistory(updated);
|
setHistory(updated);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -231,6 +344,8 @@ function ScriptTokensPage() {
|
|||||||
const grade = result ? getGrade(result.totalScore) : null;
|
const grade = result ? getGrade(result.totalScore) : null;
|
||||||
const beatPct = result ? (result.totalScore >= 95 ? 97 : result.totalScore >= 88 ? 92 : result.totalScore >= 80 ? 85 : 72) : 0;
|
const beatPct = result ? (result.totalScore >= 95 ? 97 : result.totalScore >= 88 ? 92 : result.totalScore >= 80 ? 85 : 72) : 0;
|
||||||
const compactTitle = uploadedFile?.name?.replace(/\.[^.]+$/, "") ?? "剧本评测";
|
const compactTitle = uploadedFile?.name?.replace(/\.[^.]+$/, "") ?? "剧本评测";
|
||||||
|
const scriptMinutes = Math.max(8, Math.round(script.length / 460));
|
||||||
|
const reportDate = new Date().toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="script-eval-v5 page-motion">
|
<section className="script-eval-v5 page-motion">
|
||||||
@@ -261,11 +376,11 @@ function ScriptTokensPage() {
|
|||||||
<button type="button" className="script-eval-v5-upload-btn" onClick={(e) => { e.stopPropagation(); fileInputRef.current?.click(); }}>
|
<button type="button" className="script-eval-v5-upload-btn" onClick={(e) => { e.stopPropagation(); fileInputRef.current?.click(); }}>
|
||||||
+ 上传剧本
|
+ 上传剧本
|
||||||
</button>
|
</button>
|
||||||
<div className="script-eval-v5-upload-hint">支持 .txt .md</div>
|
<div className="script-eval-v5-upload-hint">{TEXT_FILE_HINT}</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<input ref={fileInputRef} type="file" accept=".txt,.md" style={{ display: "none" }} onChange={handleFileUpload} />
|
<input ref={fileInputRef} type="file" accept={TEXT_FILE_ACCEPT} style={{ display: "none" }} onChange={handleFileUpload} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="script-eval-v5-lp-section">
|
<div className="script-eval-v5-lp-section">
|
||||||
@@ -359,45 +474,31 @@ function ScriptTokensPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="script-eval-v5-right-content">
|
<div className={`script-eval-v5-right-content${result ? " is-report" : ""}`}>
|
||||||
{!result && (
|
{!result && (
|
||||||
<div className="script-eval-v5-input-section">
|
<div className="script-eval-v5-input-section">
|
||||||
{/* Script-themed upload illustration */}
|
<div className="script-eval-v5-illustration" aria-label="上传剧本并开始评测">
|
||||||
<div
|
<div
|
||||||
className="script-eval-v5-illustration"
|
className="script-eval-v5-illustration-hit"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
onKeyDown={uploadKeyDown}
|
onKeyDown={uploadKeyDown}
|
||||||
>
|
>
|
||||||
<div className="script-eval-v5-illust-grid">
|
<div className="script-eval-v5-upload-card-icon">
|
||||||
{[0, 1, 2, 3, 4, 5].map((idx) => (
|
<FileTextOutlined />
|
||||||
<div key={idx} className={`script-eval-v5-illust-page${idx === 1 ? " is-active" : ""}`}>
|
</div>
|
||||||
<div className="script-eval-v5-illust-page-lines">
|
<div className="script-eval-v5-upload-card-title">
|
||||||
<div className="script-eval-v5-illust-line" style={{ width: `${60 + Math.sin(idx * 1.2) * 20}%` }} />
|
{uploadedFile ? "剧本已导入" : "上传剧本文件"}
|
||||||
<div className="script-eval-v5-illust-line" style={{ width: `${75 + Math.cos(idx * 1.7) * 15}%` }} />
|
</div>
|
||||||
<div className="script-eval-v5-illust-line" style={{ width: `${45 + Math.sin(idx * 2.1) * 25}%` }} />
|
<div className="script-eval-v5-upload-card-desc">
|
||||||
<div className="script-eval-v5-illust-line" style={{ width: `${65 + Math.cos(idx * 1.3) * 20}%` }} />
|
{uploadedFile
|
||||||
<div className="script-eval-v5-illust-line is-short" style={{ width: `${35 + Math.sin(idx * 0.8) * 15}%` }} />
|
? "如需更换,点击此处重新上传;完成后点击左侧开始评测。"
|
||||||
</div>
|
: `${TEXT_FILE_HINT},上传后点击开始评测,AI 将识别剧本信息。`}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="script-eval-v5-illust-label">
|
|
||||||
<FileTextOutlined />
|
|
||||||
<span>上传或粘贴剧本开始评测</span>
|
|
||||||
</div>
|
|
||||||
<div className="script-eval-v5-illust-hint">支持 TXT / MD 格式,点击此处或左侧上传区导入剧本文件</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="script-eval-v5-textarea-shell">
|
|
||||||
<textarea
|
|
||||||
className="script-eval-v5-textarea"
|
|
||||||
value={script}
|
|
||||||
onChange={(e) => setScript(e.target.value)}
|
|
||||||
placeholder={"或直接在此粘贴剧本内容...\n\n【第一幕】夜晚,城市天台。霓虹灯映照着雨后的地面。\n小凯独自站在天台边缘,手中握着一张皱巴巴的纸条..."}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{evalError && (
|
{evalError && (
|
||||||
<div className="script-eval-v5-error" role="alert">
|
<div className="script-eval-v5-error" role="alert">
|
||||||
<span>⚠</span><span>{evalError}</span>
|
<span>⚠</span><span>{evalError}</span>
|
||||||
@@ -407,153 +508,131 @@ function ScriptTokensPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{result && (
|
{result && (
|
||||||
<>
|
<section className="script-eval-report script-eval-report--inside">
|
||||||
<div className="script-eval-v5-hero">
|
<div className="script-eval-report__body">
|
||||||
<div className="script-eval-v5-hero-top">
|
<header className="script-eval-report__hero">
|
||||||
<span className="script-eval-v5-hero-num">{animatedScore}</span>
|
<div className="script-eval-report__score-block">
|
||||||
<span className="script-eval-v5-hero-total">/ 100</span>
|
<div className="script-eval-report__score-row">
|
||||||
<div className="script-eval-v5-hero-grade">
|
<span className="script-eval-report__score">{animatedScore}</span>
|
||||||
<span className="script-eval-v5-hero-grade-dot" />
|
<span className="script-eval-report__score-total">/ 100</span>
|
||||||
<span>{grade}级</span>
|
<span className="script-eval-report__grade">
|
||||||
|
<i />
|
||||||
|
{grade}级
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="script-eval-report__score-line">
|
||||||
|
<span style={{ width: `${animatedScore}%` }} />
|
||||||
|
</div>
|
||||||
|
<div className="script-eval-report__beat">击败全国 <b>{beatPct}%</b> 剧本</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="script-eval-v5-hero-bar">
|
|
||||||
<div className="script-eval-v5-hero-bar-fill" style={{ width: `${animatedScore}%` }} />
|
|
||||||
</div>
|
|
||||||
<div className="script-eval-v5-hero-beat">击败全国 <b>{beatPct}%</b> 剧本</div>
|
|
||||||
<div className="script-eval-v5-hero-title">{compactTitle}</div>
|
|
||||||
<div className="script-eval-v5-hero-desc">{result.summary}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="script-eval-v5-card">
|
<div className="script-eval-report__summary">
|
||||||
<div className="script-eval-v5-card-head">
|
<div className="script-eval-report__title-line">
|
||||||
<div className="script-eval-v5-card-head-left">
|
<div>
|
||||||
<div className="script-eval-v5-ch-dot" />
|
<h1>{compactTitle}</h1>
|
||||||
<div className="script-eval-v5-ch-title">六维评分</div>
|
<p>{`剧本评测 · ${scriptMinutes} min · ${reportDate}`}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="script-eval-report__desc">{result.summary}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="script-eval-v5-ch-legend">
|
</header>
|
||||||
<div className="script-eval-v5-leg"><div className="script-eval-v5-ldot is-score" />得分</div>
|
|
||||||
<div className="script-eval-v5-leg"><div className="script-eval-v5-ldot is-loss" />扣分</div>
|
<section className="script-eval-report__chart-card" aria-label="维度拆解">
|
||||||
|
<div className="script-eval-report__card-head">
|
||||||
|
<span><i />维度拆解</span>
|
||||||
|
<div className="script-eval-report__legend">
|
||||||
|
<span><i className="is-score" />得分</span>
|
||||||
|
<span><i className="is-loss" />扣分</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="script-eval-report__chart">
|
||||||
<div className="script-eval-v5-card-body">
|
<div className="script-eval-report__axis">
|
||||||
<div className="script-eval-v5-chart-container">
|
<span>100%</span>
|
||||||
<div className="script-eval-v5-chart-bars">
|
<span>80%</span>
|
||||||
{SCORE_DIMENSIONS.map((dim, i) => {
|
<span>60%</span>
|
||||||
|
<span>40%</span>
|
||||||
|
<span>20%</span>
|
||||||
|
<span>0%</span>
|
||||||
|
</div>
|
||||||
|
<div className="script-eval-report__chart-grid">
|
||||||
|
{SCORE_DIMENSIONS.map((dim) => {
|
||||||
const score = result.dimensionScores[dim.key] ?? 0;
|
const score = result.dimensionScores[dim.key] ?? 0;
|
||||||
const pct = score / dim.maxScore;
|
const pct = Math.max(0, Math.min(1, score / dim.maxScore));
|
||||||
const lossPct = (dim.maxScore - score) / dim.maxScore;
|
const lossPct = 1 - pct;
|
||||||
const isPerfect = score === dim.maxScore;
|
const isPerfect = score === dim.maxScore;
|
||||||
return (
|
return (
|
||||||
<div
|
<button key={dim.key} type="button" className="script-eval-report__bar-col">
|
||||||
key={dim.key}
|
<div className="script-eval-report__bar-score">
|
||||||
className={`script-eval-v5-bcol${activeDim === i ? " is-active" : ""}${activeDim !== null && activeDim !== i ? " is-dimmed" : ""}`}
|
<b>{score}</b><small>/{dim.maxScore}</small>{isPerfect ? <em>*</em> : null}
|
||||||
onClick={() => setActiveDim(activeDim === i ? null : i)}
|
|
||||||
>
|
|
||||||
<div className="script-eval-v5-bbar-area">
|
|
||||||
{lossPct > 0 && (
|
|
||||||
<div className="script-eval-v5-bseg is-loss" style={{ height: `${lossPct * 80}%`, transitionDelay: `${i * 80}ms` }} />
|
|
||||||
)}
|
|
||||||
<div className={`script-eval-v5-bseg is-score${isPerfect ? " is-perfect" : ""}`} style={{ height: `${pct * 80}%`, transitionDelay: `${i * 80}ms` }} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="script-eval-v5-bscore-label">
|
<div className="script-eval-report__bar-box">
|
||||||
{score}<span className="script-eval-v5-bmax">/{dim.maxScore}</span>
|
{lossPct > 0 ? <div className="script-eval-report__bar-loss" style={{ height: `${lossPct * 100}%` }} /> : null}
|
||||||
{isPerfect && <span className="script-eval-v5-bstar"> ★</span>}
|
<div className="script-eval-report__bar-fill" style={{ height: `${pct * 100}%` }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<strong>{dim.label}</strong>
|
||||||
|
<span>{dim.hint}</span>
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="script-eval-v5-chart-bottom">
|
|
||||||
<div className="script-eval-v5-chart-dims">
|
|
||||||
{SCORE_DIMENSIONS.map((dim, i) => (
|
|
||||||
<div
|
|
||||||
key={dim.key}
|
|
||||||
className={`script-eval-v5-chart-dim${activeDim === i ? " is-active" : ""}${activeDim !== null && activeDim !== i ? " is-dimmed" : ""}`}
|
|
||||||
onClick={() => setActiveDim(activeDim === i ? null : i)}
|
|
||||||
>
|
|
||||||
<div className="script-eval-v5-chart-dim-name">{dim.label}</div>
|
|
||||||
<div className="script-eval-v5-chart-dim-hint">{dim.hint}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{activeDim !== null && (() => {
|
</section>
|
||||||
const d = SCORE_DIMENSIONS[activeDim]!;
|
|
||||||
const s = result.dimensionScores[d.key] ?? 0;
|
|
||||||
return (
|
|
||||||
<div className="script-eval-v5-dim-overlay is-open">
|
|
||||||
<button className="script-eval-v5-dim-overlay-close" onClick={() => setActiveDim(null)}>✕</button>
|
|
||||||
<div className="script-eval-v5-do-inner">
|
|
||||||
<div className="script-eval-v5-do-left">
|
|
||||||
<div className="script-eval-v5-do-name">{d.label}</div>
|
|
||||||
<div className="script-eval-v5-do-score">{s}<span className="script-eval-v5-do-max">/{d.maxScore}</span></div>
|
|
||||||
<div className="script-eval-v5-do-bar"><div className="script-eval-v5-do-bar-fill" style={{ width: `${Math.round(s / d.maxScore * 100)}%` }} /></div>
|
|
||||||
<div className="script-eval-v5-do-hint">{d.hint}</div>
|
|
||||||
</div>
|
|
||||||
<div className="script-eval-v5-do-right"><div className="script-eval-v5-do-detail">{d.detail}</div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(result.highlights.length > 0 || result.issues.length > 0) && (
|
<div className="script-eval-report__findings">
|
||||||
<div className="script-eval-v5-findings">
|
{result.highlights.length > 0 ? (
|
||||||
{result.highlights.length > 0 && (
|
<section className="script-eval-report__finding-group is-highlight">
|
||||||
<div className="script-eval-v5-find-group">
|
<h2>亮点 <span>{result.highlights.length}</span></h2>
|
||||||
<div className="script-eval-v5-find-group-label is-green">
|
<div>
|
||||||
✦ 亮点 <span className="script-eval-v5-fg-count">{result.highlights.length}</span>
|
{result.highlights.map((item, index) => (
|
||||||
</div>
|
<p key={index}>{item}</p>
|
||||||
<div className="script-eval-v5-fi-list">
|
|
||||||
{result.highlights.map((h, i) => (
|
|
||||||
<div key={i} className="script-eval-v5-fi-item is-highlight"><div className="script-eval-v5-fi-marker" /><div>{h}</div></div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
)}
|
) : null}
|
||||||
{result.issues.length > 0 && (
|
{result.issues.length > 0 ? (
|
||||||
<div className="script-eval-v5-find-group">
|
<section className="script-eval-report__finding-group is-issue">
|
||||||
<div className="script-eval-v5-find-group-label is-orange">
|
<h2>扣分点 <span>{result.issues.length}</span></h2>
|
||||||
⚠ 扣分点 <span className="script-eval-v5-fg-count">{result.issues.length}</span>
|
<div>
|
||||||
</div>
|
{result.issues.map((item, index) => (
|
||||||
<div className="script-eval-v5-fi-list">
|
<p key={index}>{item}</p>
|
||||||
{result.issues.map((issue, i) => (
|
|
||||||
<div key={i} className="script-eval-v5-fi-item is-issue"><div className="script-eval-v5-fi-marker" /><div>{issue}</div></div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{result.suggestions.length > 0 && (
|
{result.suggestions.length > 0 ? (
|
||||||
<div className="script-eval-v5-card">
|
<section className="script-eval-report__path-card">
|
||||||
<div className="script-eval-v5-card-head">
|
<div className="script-eval-report__card-head">
|
||||||
<div className="script-eval-v5-card-head-left"><div className="script-eval-v5-ch-dot" /><div className="script-eval-v5-ch-title">优化路径</div></div>
|
<span><i />优化路径</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="script-eval-v5-card-body">
|
<table className="script-eval-report__path-table">
|
||||||
<table className="script-eval-v5-sug-table">
|
<thead>
|
||||||
<thead><tr><th style={{ width: 60 }}>优先级</th><th style={{ width: 68 }}>类型</th><th>优化建议</th></tr></thead>
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>优化路径</th>
|
||||||
|
<th>优先级</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{result.suggestions.map((s, i) => {
|
{result.suggestions.map((item, index) => {
|
||||||
const isHigh = i < 2;
|
const high = index < 2;
|
||||||
return (
|
return (
|
||||||
<tr key={i} className={isHigh ? "is-high" : "is-mid"}>
|
<tr key={index}>
|
||||||
<td><span className={`script-eval-v5-sug-priority${isHigh ? " is-high" : " is-mid"}`}>{isHigh ? "HIGH" : "MID"}</span></td>
|
<td>{String(index + 1).padStart(2, "0")}</td>
|
||||||
<td><div className="script-eval-v5-sug-type">{isHigh ? "核心" : "增强"}</div></td>
|
<td>{high ? "核心" : "增强"}</td>
|
||||||
<td>{s}</td>
|
<td>{item}</td>
|
||||||
|
<td><span className={high ? "is-high" : "is-mid"}>{high ? "HIGH" : "MID"}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
) : null}
|
||||||
)}
|
</div>
|
||||||
</>
|
</section>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user