feat: 首页增加工具箱功能区、剧本评测可视化展示;重构剧本评分页面UI
- 首页新增工具箱功能区(ToolboxSection),展示四大AI工具卡片 - 首页剧本功能区替换为六维柱状图可视化(ScriptReviewVisual) - 剧本评分页面(ScriptTokensPage)全面重构为新版UI布局 - 左侧面板:上传区、AI识别信息、历史评测(持久化)、操作按钮 - 右侧:剧本输入区、评测结果Hero、六维柱状图、亮点/扣分点、优化建议表格 - 历史评测支持localStorage持久化,按时间倒序排列
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const DIMS = [
|
||||
{ name: "钩子设计", score: 19, max: 20, hue: 145 },
|
||||
{ name: "角色塑造", score: 13, max: 15, hue: 155 },
|
||||
{ name: "剧情结构", score: 18, max: 20, hue: 165 },
|
||||
{ name: "逻辑严密", score: 14, max: 15, hue: 175 },
|
||||
{ name: "场景构建", score: 15, max: 15, hue: 185 },
|
||||
{ name: "内容深度", score: 15, max: 15, hue: 195 },
|
||||
];
|
||||
|
||||
function ScriptReviewVisual() {
|
||||
const [animated, setAnimated] = useState(false);
|
||||
const [activeDim, setActiveDim] = useState<number | null>(null);
|
||||
const [score, setScore] = useState(0);
|
||||
const scoreRef = useRef<number>(0);
|
||||
const frameRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = document.getElementById("script-review-visual");
|
||||
if (!el) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry?.isIntersecting) {
|
||||
setAnimated(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!animated) return;
|
||||
const start = performance.now();
|
||||
const target = 94;
|
||||
const dur = 1400;
|
||||
function tick(now: number) {
|
||||
const t = Math.min((now - start) / dur, 1);
|
||||
const e = 1 - Math.pow(1 - t, 3);
|
||||
setScore(Math.round(e * target));
|
||||
if (t < 1) frameRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
frameRef.current = requestAnimationFrame(tick);
|
||||
return () => { if (frameRef.current) cancelAnimationFrame(frameRef.current); };
|
||||
}, [animated]);
|
||||
|
||||
const totalScore = 94;
|
||||
const grade = "S";
|
||||
|
||||
return (
|
||||
<div className="omni-script-review-visual" id="script-review-visual">
|
||||
<div className="omni-script-review-hero">
|
||||
<div className="omni-script-review-score-row">
|
||||
<span className="omni-script-review-num">{score}</span>
|
||||
<span className="omni-script-review-total">/ 100</span>
|
||||
<div className="omni-script-review-grade">
|
||||
<span className="omni-script-review-grade-dot" />
|
||||
<span>{grade}级</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="omni-script-review-bar">
|
||||
<div
|
||||
className="omni-script-review-bar-fill"
|
||||
style={{ width: animated ? `${totalScore}%` : "0%" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="omni-script-review-beat">
|
||||
击败全国 <b>92%</b> 剧本
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="omni-script-review-chart">
|
||||
<div className="omni-script-review-chart-bars">
|
||||
{DIMS.map((dim, i) => {
|
||||
const pct = dim.score / dim.max;
|
||||
const lossPct = (dim.max - dim.score) / dim.max;
|
||||
const isPerfect = dim.score === dim.max;
|
||||
const height = animated ? pct * 76 : 0;
|
||||
const lossHeight = animated ? lossPct * 76 : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={dim.name}
|
||||
className={`omni-script-review-bcol${activeDim === i ? " is-active" : ""}${activeDim !== null && activeDim !== i ? " is-dimmed" : ""}`}
|
||||
onClick={() => setActiveDim(activeDim === i ? null : i)}
|
||||
>
|
||||
<div className="omni-script-review-bbar-area">
|
||||
{lossPct > 0 && (
|
||||
<div
|
||||
className="omni-script-review-bseg is-loss"
|
||||
style={{ height: `${lossHeight}%`, transitionDelay: `${400 + i * 80}ms` }}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`omni-script-review-bseg is-score${isPerfect ? " is-perfect" : ""}`}
|
||||
style={{ height: `${height}%`, transitionDelay: `${400 + i * 80}ms` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="omni-script-review-blabel">
|
||||
<span>{dim.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{activeDim !== null && (() => {
|
||||
const d = DIMS[activeDim]!;
|
||||
return (
|
||||
<div className="omni-script-review-diminfo">
|
||||
<span className="omni-script-review-diminfo-name">{d.name}</span>
|
||||
<span className="omni-script-review-diminfo-score">
|
||||
{d.score}<small>/{d.max}</small>
|
||||
{d.score === d.max && " ★"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="omni-script-review-legend">
|
||||
<span><span className="omni-script-review-legend-dot is-score" /> 得分</span>
|
||||
<span><span className="omni-script-review-legend-dot is-loss" /> 扣分</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ScriptReviewVisual;
|
||||
Reference in New Issue
Block a user