05e4f5b4b3
- 首页新增工具箱功能区(ToolboxSection),展示四大AI工具卡片 - 首页剧本功能区替换为六维柱状图可视化(ScriptReviewVisual) - 剧本评分页面(ScriptTokensPage)全面重构为新版UI布局 - 左侧面板:上传区、AI识别信息、历史评测(持久化)、操作按钮 - 右侧:剧本输入区、评测结果Hero、六维柱状图、亮点/扣分点、优化建议表格 - 历史评测支持localStorage持久化,按时间倒序排列
134 lines
4.7 KiB
TypeScript
134 lines
4.7 KiB
TypeScript
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;
|