feat: 首页增加工具箱功能区、剧本评测可视化展示;重构剧本评分页面UI
- 首页新增工具箱功能区(ToolboxSection),展示四大AI工具卡片 - 首页剧本功能区替换为六维柱状图可视化(ScriptReviewVisual) - 剧本评分页面(ScriptTokensPage)全面重构为新版UI布局 - 左侧面板:上传区、AI识别信息、历史评测(持久化)、操作按钮 - 右侧:剧本输入区、评测结果Hero、六维柱状图、亮点/扣分点、优化建议表格 - 历史评测支持localStorage持久化,按时间倒序排列
This commit is contained in:
@@ -8,7 +8,10 @@ import {
|
||||
ThunderboltOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
||||
import type { WebViewKey, WebImageWorkbenchTool } from "../../types";
|
||||
import WelcomeSplash from "./WelcomeSplash";
|
||||
import ToolboxSection from "./ToolboxSection";
|
||||
import ScriptReviewVisual from "./ScriptReviewVisual";
|
||||
|
||||
const OSS_MUBAN = "https://stringtest.oss-cn-hangzhou.aliyuncs.com/muban";
|
||||
const heroImage1 = `${OSS_MUBAN}/hero-1.png`;
|
||||
@@ -24,6 +27,8 @@ interface HomePageProps {
|
||||
onOpenEcommerce: () => void;
|
||||
onOpenScriptReview?: () => void;
|
||||
onOpenTokenMonitor?: () => void;
|
||||
onSelectView: (view: WebViewKey) => void;
|
||||
onOpenImageTool?: (tool: WebImageWorkbenchTool) => void;
|
||||
}
|
||||
|
||||
const HOME_BACKGROUND_VIDEO = "https://stringtest.oss-cn-hangzhou.aliyuncs.com/%E6%A0%B7%E7%89%87.mp4";
|
||||
@@ -112,7 +117,7 @@ function getHomeCarouselCardStyle(offset: number): CSSProperties {
|
||||
} as CSSProperties;
|
||||
}
|
||||
|
||||
function HomePage({ onOpenGenerate, onOpenEcommerce, onOpenScriptReview, onOpenTokenMonitor }: HomePageProps) {
|
||||
function HomePage({ onOpenGenerate, onOpenEcommerce, onOpenScriptReview, onOpenTokenMonitor, onSelectView, onOpenImageTool }: HomePageProps) {
|
||||
const [splashDismissed, setSplashDismissed] = useState(() => sessionStorage.getItem("omniai:splash-seen") === "1");
|
||||
const [activeSlideIndex, setActiveSlideIndex] = useState(0);
|
||||
const [carouselMotion, setCarouselMotion] = useState<HomeCarouselMotion | null>(null);
|
||||
@@ -296,7 +301,11 @@ function HomePage({ onOpenGenerate, onOpenEcommerce, onOpenScriptReview, onOpenT
|
||||
</button>
|
||||
</div>
|
||||
<div className="omni-home__feature-visual" aria-hidden="true">
|
||||
<img src={feature.imageUrl} alt="" />
|
||||
{feature.key === "script" ? (
|
||||
<ScriptReviewVisual />
|
||||
) : (
|
||||
<img src={feature.imageUrl} alt="" />
|
||||
)}
|
||||
</div>
|
||||
<div className="omni-home__feature-stats" aria-hidden="true">
|
||||
{feature.stats.map((item) => (
|
||||
@@ -338,6 +347,8 @@ function HomePage({ onOpenGenerate, onOpenEcommerce, onOpenScriptReview, onOpenT
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ToolboxSection onSelectView={onSelectView} onOpenImageTool={onOpenImageTool} />
|
||||
</main>
|
||||
</section>
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,233 @@
|
||||
import { ToolOutlined } from "@ant-design/icons";
|
||||
import type { WebViewKey, WebImageWorkbenchTool } from "../../types";
|
||||
import toolImageBefore from "../../assets/toolbox/牛仔.png";
|
||||
import toolImageAfter from "../../assets/toolbox/西装.png";
|
||||
import watermarkBefore from "../../assets/toolbox/去水印前.png";
|
||||
import watermarkAfter from "../../assets/toolbox/去水印后.png";
|
||||
|
||||
interface ToolboxSectionProps {
|
||||
onSelectView: (view: WebViewKey) => void;
|
||||
onOpenImageTool?: (tool: WebImageWorkbenchTool) => void;
|
||||
}
|
||||
|
||||
const TOOLS = [
|
||||
{
|
||||
key: "image-studio",
|
||||
icon: "🎨",
|
||||
name: "图片工作室",
|
||||
desc: "图片二次加工,调色裁剪特效风格迁移",
|
||||
},
|
||||
{
|
||||
key: "lens-lab",
|
||||
icon: "📷",
|
||||
name: "镜头实验室",
|
||||
desc: "多视角镜头生成,不同角度与姿势",
|
||||
},
|
||||
{
|
||||
key: "digital-human",
|
||||
icon: "🧑",
|
||||
name: "一键数字人",
|
||||
desc: "上传图片和音频,生成数字人视频",
|
||||
},
|
||||
{
|
||||
key: "watermark-removal",
|
||||
icon: "✨",
|
||||
name: "去除水印",
|
||||
desc: "AI智能识别去除图片视频水印",
|
||||
},
|
||||
];
|
||||
|
||||
const CARDS = [
|
||||
{
|
||||
key: "image-studio",
|
||||
title: "图片工作室",
|
||||
tag: "图片加工",
|
||||
icon: "🎨",
|
||||
features: ["二次加工", "调色", "裁剪", "风格迁移"],
|
||||
targetView: "imageWorkbench" as WebViewKey,
|
||||
render: () => (
|
||||
<div className="toolbox-card1-content">
|
||||
<div className="toolbox-card1-side toolbox-card1-left">
|
||||
<div className="toolbox-card1-img">
|
||||
<img src={toolImageBefore} alt="图片加工前" />
|
||||
</div>
|
||||
<div className="toolbox-card1-label">原始图片</div>
|
||||
</div>
|
||||
<div className="toolbox-card1-divider" />
|
||||
<div className="toolbox-card1-side toolbox-card1-right">
|
||||
<div className="toolbox-card1-img">
|
||||
<img src={toolImageAfter} alt="图片加工后" />
|
||||
</div>
|
||||
<div className="toolbox-card1-label">处理后</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "lens-lab",
|
||||
title: "镜头实验室",
|
||||
tag: "多视角",
|
||||
icon: "📷",
|
||||
features: ["正面", "45°侧", "俯拍", "仰拍", "背面"],
|
||||
targetView: "imageWorkbench" as WebViewKey,
|
||||
render: () => (
|
||||
<div className="toolbox-card2-content">
|
||||
{["正面", "45°侧", "俯拍", "仰拍", "背面"].map((angle) => (
|
||||
<div key={angle} className="toolbox-card2-frame">
|
||||
<div className="toolbox-card2-product" />
|
||||
<div className="toolbox-card2-shadow" />
|
||||
<div className="toolbox-card2-angle-label">{angle}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "digital-human",
|
||||
title: "一键数字人",
|
||||
tag: "视频生成",
|
||||
icon: "🧑",
|
||||
features: ["上传人像", "匹配音频", "唇形同步", "生成视频"],
|
||||
targetView: "digitalHuman" as WebViewKey,
|
||||
render: () => (
|
||||
<div className="toolbox-card3-content">
|
||||
<div className="toolbox-card3-side toolbox-card3-left">
|
||||
<div className="toolbox-card3-portrait">
|
||||
<div className="toolbox-card3-portrait-mark">STATIC</div>
|
||||
</div>
|
||||
<div className="toolbox-card3-label">静态人像</div>
|
||||
</div>
|
||||
<div className="toolbox-card3-divider" />
|
||||
<div className="toolbox-card3-transform">⚡</div>
|
||||
<div className="toolbox-card3-side toolbox-card3-right">
|
||||
<div className="toolbox-card3-portrait">
|
||||
<div className="toolbox-card3-glow-ring" />
|
||||
<div className="toolbox-card3-lipsync">
|
||||
<span /><span /><span /><span /><span />
|
||||
</div>
|
||||
<div className="toolbox-card3-gesture" />
|
||||
<div className="toolbox-card3-live">LIVE</div>
|
||||
</div>
|
||||
<div className="toolbox-card3-label">数字人视频</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "watermark-removal",
|
||||
title: "去除水印",
|
||||
tag: "AI清除",
|
||||
icon: "✨",
|
||||
features: ["智能识别", "精准去除", "无损画质"],
|
||||
targetView: "watermarkRemoval" as WebViewKey,
|
||||
render: () => (
|
||||
<div className="toolbox-card4-content">
|
||||
<div className="toolbox-card4-side toolbox-card4-left">
|
||||
<div className="toolbox-card4-img">
|
||||
<img src={watermarkBefore} alt="去水印前" />
|
||||
</div>
|
||||
<div className="toolbox-card4-label">含水印</div>
|
||||
</div>
|
||||
<div className="toolbox-card4-divider" />
|
||||
<div className="toolbox-card4-side toolbox-card4-right">
|
||||
<div className="toolbox-card4-img">
|
||||
<img src={watermarkAfter} alt="去水印后" />
|
||||
</div>
|
||||
<div className="toolbox-card4-label">已清除</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function ToolboxSection({ onSelectView, onOpenImageTool }: ToolboxSectionProps) {
|
||||
const handleCardClick = (targetView: WebViewKey) => {
|
||||
onSelectView(targetView);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="omni-home__toolbox-page" aria-label="OmniAI 工具箱">
|
||||
<div className="omni-home__toolbox-shell">
|
||||
{/* Left Panel */}
|
||||
<aside className="omni-home__toolbox-left">
|
||||
<div className="omni-home__toolbox-brand">
|
||||
<div className="omni-home__toolbox-brand-icon">
|
||||
<ToolOutlined />
|
||||
</div>
|
||||
<div className="omni-home__toolbox-brand-text">工具箱</div>
|
||||
</div>
|
||||
<div className="omni-home__toolbox-title">
|
||||
专业工具
|
||||
<br />
|
||||
精准创作
|
||||
</div>
|
||||
<div className="omni-home__toolbox-subtitle">
|
||||
四大AI工具覆盖图片加工、镜头变换、数字人制作、水印处理全流程
|
||||
</div>
|
||||
<div className="omni-home__toolbox-list">
|
||||
{TOOLS.map((tool) => (
|
||||
<div
|
||||
key={tool.key}
|
||||
className="omni-home__toolbox-item"
|
||||
onClick={() => {
|
||||
const card = CARDS.find((c) => c.key === tool.key);
|
||||
if (card) handleCardClick(card.targetView);
|
||||
}}
|
||||
>
|
||||
<div className="omni-home__toolbox-item-icon">{tool.icon}</div>
|
||||
<div className="omni-home__toolbox-item-info">
|
||||
<div className="omni-home__toolbox-item-name">{tool.name}</div>
|
||||
<div className="omni-home__toolbox-item-desc">{tool.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="omni-home__toolbox-workflow">
|
||||
<div className="omni-home__toolbox-workflow-label">通用工作流</div>
|
||||
<div className="omni-home__toolbox-workflow-steps">
|
||||
<span className="omni-home__toolbox-workflow-step">上传素材</span>
|
||||
<span className="omni-home__toolbox-workflow-arrow">→</span>
|
||||
<span className="omni-home__toolbox-workflow-step">选择工具</span>
|
||||
<span className="omni-home__toolbox-workflow-arrow">→</span>
|
||||
<span className="omni-home__toolbox-workflow-step">AI处理</span>
|
||||
<span className="omni-home__toolbox-workflow-arrow">→</span>
|
||||
<span className="omni-home__toolbox-workflow-step">导出成果</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Grid Area */}
|
||||
<div className="omni-home__toolbox-grid">
|
||||
{CARDS.map((card) => (
|
||||
<div
|
||||
key={card.key}
|
||||
className="omni-home__toolbox-card"
|
||||
onClick={() => handleCardClick(card.targetView)}
|
||||
>
|
||||
<div className="omni-home__toolbox-card-header">
|
||||
<div className="omni-home__toolbox-card-header-left">
|
||||
<div className="omni-home__toolbox-card-icon">{card.icon}</div>
|
||||
<div className="omni-home__toolbox-card-title">{card.title}</div>
|
||||
</div>
|
||||
<div className="omni-home__toolbox-card-tag">{card.tag}</div>
|
||||
</div>
|
||||
<div className="omni-home__toolbox-card-content">
|
||||
{card.render()}
|
||||
</div>
|
||||
<div className="omni-home__toolbox-card-footer">
|
||||
{card.features.map((feat, i) => (
|
||||
<span key={feat}>
|
||||
{i > 0 && <span className="omni-home__toolbox-card-feat-sep">|</span>}
|
||||
<span className="omni-home__toolbox-card-feat">{feat}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToolboxSection;
|
||||
Reference in New Issue
Block a user