135 lines
4.7 KiB
TypeScript
135 lines
4.7 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import "../../styles/pages/script-review-visual.css";
|
|
|
|
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;
|