209 lines
8.9 KiB
TypeScript
209 lines
8.9 KiB
TypeScript
|
|
import { useEffect, useRef, useState } from "react";
|
|||
|
|
|
|||
|
|
const DIMS = [
|
|||
|
|
{ name: "钩子设计", score: 16, max: 20, hue: 145, desc: "吸引力·悬念·黄金三秒", isPerfect: false, isLow: false },
|
|||
|
|
{ name: "角色塑造", score: 15, max: 15, hue: 155, desc: "立体度·动机·弧光", isPerfect: true, isLow: false },
|
|||
|
|
{ name: "剧情结构", score: 16, max: 20, hue: 165, desc: "起承转合·节奏·冲突", isPerfect: false, isLow: false },
|
|||
|
|
{ name: "逻辑严密", score: 12, max: 15, hue: 175, desc: "自洽·伏笔·因果链", isPerfect: false, isLow: false },
|
|||
|
|
{ name: "场景构建", score: 10, max: 15, hue: 185, desc: "空间·视听·画面感", isPerfect: false, isLow: true },
|
|||
|
|
{ name: "内容深度", score: 8, max: 15, hue: 195, desc: "主题·情感·思想内核", isPerfect: false, isLow: true },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const HIGHLIGHTS = [
|
|||
|
|
{ dim: "角色塑造", score: "15/15 ★", text: "满分表现!人物立体度与弧光设计均达高水准" },
|
|||
|
|
{ dim: "钩子设计", score: "16/20", text: "开篇悬念精准,黄金三秒有效抓住注意力" },
|
|||
|
|
{ dim: "剧情结构", score: "16/20", text: "起承转合清晰,节奏把控得当,冲突自然递进" },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const WEAKNESSES = [
|
|||
|
|
{ dim: "内容深度", score: "8/15", text: "主题偏表层,情感共鸣与思想内核挖掘不足" },
|
|||
|
|
{ dim: "场景构建", score: "10/15", text: "空间描写模糊,视听语言薄弱,画面感欠缺" },
|
|||
|
|
{ dim: "逻辑严密", score: "12/15", text: "世界观细节不足,部分伏笔缺乏回收" },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const OPTIMIZATIONS = [
|
|||
|
|
{ dim: "场景构建 → 提升", priority: "高优先", priorityClass: "badge-red", text: "增加具体空间描写与视听语言,强化沉浸感" },
|
|||
|
|
{ dim: "内容深度 → 深挖", priority: "高优先", priorityClass: "badge-red", text: "围绕核心冲突深化人物选择与情感层次" },
|
|||
|
|
{ dim: "逻辑严密 → 补强", priority: "中优先", priorityClass: "badge-orange", text: "补充世界观细节,强化因果链与伏笔回收" },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
function animateNumber(el: HTMLElement | null, target: number, duration: number) {
|
|||
|
|
if (!el) return;
|
|||
|
|
const start = performance.now();
|
|||
|
|
const targetEl = el;
|
|||
|
|
function tick(now: number) {
|
|||
|
|
const p = Math.min((now - start) / duration, 1);
|
|||
|
|
targetEl.textContent = String(Math.round((1 - Math.pow(1 - p, 3)) * target));
|
|||
|
|
if (p < 1) requestAnimationFrame(tick);
|
|||
|
|
}
|
|||
|
|
requestAnimationFrame(tick);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ScriptReviewShowcase() {
|
|||
|
|
const [animated, setAnimated] = useState(false);
|
|||
|
|
const scoreRef = useRef<HTMLSpanElement>(null);
|
|||
|
|
const barRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|||
|
|
const scoreValRefs = useRef<(HTMLSpanElement | null)[]>([]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const el = document.getElementById("script-review-showcase");
|
|||
|
|
if (!el) return;
|
|||
|
|
const observer = new IntersectionObserver(
|
|||
|
|
([entry]) => {
|
|||
|
|
if (entry?.isIntersecting) {
|
|||
|
|
setAnimated(true);
|
|||
|
|
observer.disconnect();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{ threshold: 0.25 },
|
|||
|
|
);
|
|||
|
|
observer.observe(el);
|
|||
|
|
return () => observer.disconnect();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!animated) return;
|
|||
|
|
const timer = setTimeout(() => {
|
|||
|
|
animateNumber(scoreRef.current, 77, 1400);
|
|||
|
|
barRefs.current.forEach((bar, i) => {
|
|||
|
|
if (!bar) return;
|
|||
|
|
const pct = parseFloat(bar.dataset.pct ?? "0");
|
|||
|
|
setTimeout(() => { bar.style.height = `${pct}%`; }, i * 100 + 400);
|
|||
|
|
});
|
|||
|
|
scoreValRefs.current.forEach((el, i) => {
|
|||
|
|
setTimeout(() => animateNumber(el, parseInt(el?.dataset.target ?? "0"), 800), i * 100 + 400);
|
|||
|
|
});
|
|||
|
|
}, 500);
|
|||
|
|
return () => clearTimeout(timer);
|
|||
|
|
}, [animated]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="omni-script-review-showcase" id="script-review-showcase">
|
|||
|
|
{/* Score Hero */}
|
|||
|
|
<div className="srs-score-hero">
|
|||
|
|
<div className="srs-score-left">
|
|||
|
|
<div className="srs-score-circle">
|
|||
|
|
<div className="srs-score-circle-inner">
|
|||
|
|
<span className="srs-score-num" ref={scoreRef}>0</span>
|
|||
|
|
<span className="srs-score-den">/ 100</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="srs-score-meta">
|
|||
|
|
<div className="srs-score-grade">A 级</div>
|
|||
|
|
<div className="srs-score-tags">
|
|||
|
|
<span className="srs-score-tag">现实剧情</span>
|
|||
|
|
<span className="srs-score-tag">58min</span>
|
|||
|
|
<span className="srs-score-tag">6角色</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="srs-score-divider" />
|
|||
|
|
<div className="srs-score-right">
|
|||
|
|
<div className="srs-score-proj">电商广告片生成项目计划 · 评测结果</div>
|
|||
|
|
<div className="srs-score-summary">
|
|||
|
|
现实剧情特征清晰,角色塑造表现突出。当前最值得继续打磨的是内容深度,建议围绕人物选择、冲突升级和可拍摄细节继续压实。
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Vertical Bar Chart */}
|
|||
|
|
<div className="srs-chart-card">
|
|||
|
|
<div className="srs-chart-title">六维评分 Dimension Breakdown</div>
|
|||
|
|
<div className="srs-chart-body">
|
|||
|
|
{DIMS.map((dim, i) => {
|
|||
|
|
const pct = dim.score / dim.max;
|
|||
|
|
return (
|
|||
|
|
<div key={dim.name} className="srs-chart-col">
|
|||
|
|
<div className="srs-chart-bar-wrap">
|
|||
|
|
<div className="srs-chart-bar-bg" style={{ height: "100%" }} />
|
|||
|
|
<div
|
|||
|
|
ref={(el) => { barRefs.current[i] = el; }}
|
|||
|
|
className={`srs-chart-bar-fill${dim.isPerfect ? " is-perfect" : ""}${dim.isLow ? " is-low" : ""}`}
|
|||
|
|
data-pct={String(Math.round(pct * 100))}
|
|||
|
|
style={{ height: "0%" }}
|
|||
|
|
>
|
|||
|
|
<div className="srs-chart-bar-score">
|
|||
|
|
<span
|
|||
|
|
ref={(el) => { scoreValRefs.current[i] = el; }}
|
|||
|
|
data-target={String(dim.score)}
|
|||
|
|
>0</span>
|
|||
|
|
<span className="srs-chart-bar-sub">/{dim.max}</span>
|
|||
|
|
{dim.isPerfect && <span className="srs-chart-bar-star">★</span>}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="srs-chart-col-label">
|
|||
|
|
<div className="srs-chart-col-name">{dim.name}</div>
|
|||
|
|
<div className="srs-chart-col-desc">{dim.desc}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Triple Section */}
|
|||
|
|
<div className="srs-triple-section">
|
|||
|
|
{/* Highlights */}
|
|||
|
|
<div className="srs-section-card is-highlight">
|
|||
|
|
<div className="srs-section-header">
|
|||
|
|
<div className="srs-section-icon">✦</div>
|
|||
|
|
<span className="srs-section-label">亮点</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="srs-section-list">
|
|||
|
|
{HIGHLIGHTS.map((item) => (
|
|||
|
|
<div key={item.dim} className="srs-section-item">
|
|||
|
|
<div className="srs-section-item-head">
|
|||
|
|
<span className="srs-section-item-dim">{item.dim}</span>
|
|||
|
|
<span className="srs-section-item-score is-green">{item.score}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="srs-section-item-text">{item.text}</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Weaknesses */}
|
|||
|
|
<div className="srs-section-card is-weakness">
|
|||
|
|
<div className="srs-section-header">
|
|||
|
|
<div className="srs-section-icon">✗</div>
|
|||
|
|
<span className="srs-section-label">缺点</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="srs-section-list">
|
|||
|
|
{WEAKNESSES.map((item) => (
|
|||
|
|
<div key={item.dim} className="srs-section-item">
|
|||
|
|
<div className="srs-section-item-head">
|
|||
|
|
<span className="srs-section-item-dim">{item.dim}</span>
|
|||
|
|
<span className="srs-section-item-score is-red">{item.score}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="srs-section-item-text">{item.text}</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Optimization */}
|
|||
|
|
<div className="srs-section-card is-optimize">
|
|||
|
|
<div className="srs-section-header">
|
|||
|
|
<div className="srs-section-icon">⚡</div>
|
|||
|
|
<span className="srs-section-label">优化路径</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="srs-section-list">
|
|||
|
|
{OPTIMIZATIONS.map((item) => (
|
|||
|
|
<div key={item.dim} className="srs-section-item">
|
|||
|
|
<div className="srs-section-item-head">
|
|||
|
|
<span className="srs-section-item-dim">{item.dim}</span>
|
|||
|
|
<span className={`srs-section-item-badge ${item.priorityClass}`}>{item.priority}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="srs-section-item-text">{item.text}</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default ScriptReviewShowcase;
|