feat(ui): 新建首页落地页
Deploy / Build Production Runtime Images (push) Has been cancelled
Deploy / Deploy Production (push) Has been cancelled
Deploy / Production Browser E2E (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Failing after 193h58m42s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 193h58m51s

- 新增公开落地页 HomePage(/),包含 5 个区域:HeroSection、FeatureSection、WorkflowSection、PricingSection、CTASection
- 受保护路由基础路径从 / 迁移至 /app,使 / 可作为公开首页
- 新增 HomeRoute 组件:已登录用户自动跳转 /app/dashboard,未登录用户展示落地页
- 级联更新所有导航路径:Sidebar、Header、PageHead 全部迁移至 /app 前缀
- 零 antd 直接导入,全部使用 CSS 变量 + V21 Design System
- 响应式适配:768px / 480px 断点

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
灵应
2026-07-01 10:55:37 +08:00
parent 89c1e42f38
commit 95d1dedcb5
6 changed files with 967 additions and 61 deletions
+18 -18
View File
@@ -40,69 +40,69 @@ const NAV_ITEMS: NavItem[] = [
{
key: "dashboard",
label: "概览",
path: "/dashboard",
path: "/app/dashboard",
icon: <DashboardOutlined />,
},
{ key: "assets", label: "素材库", path: "/assets", icon: <FileOutlined /> },
{ key: "assets", label: "素材库", path: "/app/assets", icon: <FileOutlined /> },
{
key: "titles",
label: "标题库",
path: "/titles",
path: "/app/titles",
icon: <FileTextOutlined />,
},
{ key: "voices", label: "配音库", path: "/voices", icon: <AudioOutlined /> },
{ key: "voices", label: "配音库", path: "/app/voices", icon: <AudioOutlined /> },
{
key: "voice-clone",
label: "我的音色",
path: "/voice-clone",
path: "/app/voice-clone",
icon: <AudioOutlined />,
},
{
key: "accounts",
label: "账号管理",
path: "/accounts",
path: "/app/accounts",
icon: <AppstoreOutlined />,
},
{
key: "templates",
label: "模板库",
path: "/templates",
path: "/app/templates",
icon: <AppstoreOutlined />,
},
{
key: "editing-planner",
label: "剪辑编辑器",
path: "/editing-planner",
path: "/app/editing-planner",
icon: <EditOutlined />,
},
{
key: "my-templates",
label: "我的模板",
path: "/my-templates",
path: "/app/my-templates",
icon: <FolderOutlined />,
},
{
key: "generate",
label: "一键生成",
path: "/generate",
path: "/app/generate",
icon: <VideoCameraOutlined />,
},
{
key: "history",
label: "任务历史",
path: "/history",
path: "/app/history",
icon: <HistoryOutlined />,
},
{
key: "products",
label: "成品库",
path: "/products",
path: "/app/products",
icon: <TrophyOutlined />,
},
{
key: "duplication",
label: "查重",
path: "/duplication",
path: "/app/duplication",
icon: <ScanOutlined />,
},
];
@@ -120,13 +120,13 @@ const Header: React.FC = () => {
key: "profile",
icon: <UserOutlined />,
label: "个人设置",
onClick: () => navigate("/profile"),
onClick: () => navigate("/app/profile"),
},
{
key: "subscription",
icon: <SettingOutlined />,
label: "订阅管理",
onClick: () => navigate("/subscription"),
onClick: () => navigate("/app/subscription"),
},
{ type: "divider" },
{
@@ -139,8 +139,8 @@ const Header: React.FC = () => {
/** 判断导航项是否激活 */
const isActive = (path: string) => {
if (path === "/dashboard") {
return location.pathname === "/" || location.pathname === "/dashboard";
if (path === "/app/dashboard") {
return location.pathname === "/app" || location.pathname === "/app/dashboard";
}
return location.pathname.startsWith(path);
};
@@ -151,7 +151,7 @@ const Header: React.FC = () => {
<button
className="xx-brand"
type="button"
onClick={() => navigate("/dashboard")}
onClick={() => navigate("/app/dashboard")}
>
<span className="xx-logo">🦐</span>
<span className="xx-brand-text"></span>
+27 -27
View File
@@ -42,39 +42,39 @@ export interface PageHeadProps {
/* ── 路由 → 标题映射(用于自动生成面包屑) ────────────────── */
const ROUTE_TITLE_MAP: Record<string, string> = {
"/dashboard": "首页",
"/generate": "一键生成",
"/assets": "素材库",
"/voices": "配音库",
"/titles": "标题库",
"/products": "成片库",
"/templates": "模板库",
"/history": "任务历史",
"/admin": "控制台",
"/admin/users": "用户管理",
"/admin/analytics": "数据分析",
"/admin/monitor": "系统监控",
"/admin/logs": "系统日志",
"/subscription": "订阅管理",
"/subscription/upgrade": "升级订阅",
"/subscription/billing": "账单管理",
"/profile": "个人设置",
"/editing-planner": "剪辑规划",
"/my-templates": "我的模板",
"/voice-clone": "我的音色",
"/accounts": "账号管理",
"/duplication": "查重",
"/duplication/results": "查重结果",
"/app/dashboard": "首页",
"/app/generate": "一键生成",
"/app/assets": "素材库",
"/app/voices": "配音库",
"/app/titles": "标题库",
"/app/products": "成片库",
"/app/templates": "模板库",
"/app/history": "任务历史",
"/app/admin": "控制台",
"/app/admin/users": "用户管理",
"/app/admin/analytics": "数据分析",
"/app/admin/monitor": "系统监控",
"/app/admin/logs": "系统日志",
"/app/subscription": "订阅管理",
"/app/subscription/upgrade": "升级订阅",
"/app/subscription/billing": "账单管理",
"/app/profile": "个人设置",
"/app/editing-planner": "剪辑规划",
"/app/my-templates": "我的模板",
"/app/voice-clone": "我的音色",
"/app/accounts": "账号管理",
"/app/duplication": "查重",
"/app/duplication/results": "查重结果",
};
/* ── 自动生成面包屑 ─────────────────────────────────────── */
/** 根据当前路径生成面包屑 */
const generateBreadcrumb = (pathname: string): BreadcrumbItem[] => {
const items: BreadcrumbItem[] = [{ label: "首页", path: "/dashboard" }];
const items: BreadcrumbItem[] = [{ label: "首页", path: "/app/dashboard" }];
// 首页本身不需要面包屑
if (pathname === "/" || pathname === "/dashboard") {
if (pathname === "/app" || pathname === "/app/dashboard") {
return items;
}
@@ -125,8 +125,8 @@ const PageHead: React.FC<PageHeadProps> = ({
const showBreadcrumb =
!hideBreadcrumb &&
breadcrumbItems.length > 1 &&
location.pathname !== "/" &&
location.pathname !== "/dashboard";
location.pathname !== "/app" &&
location.pathname !== "/app/dashboard";
return (
<header className="xx-page-head">
+14 -14
View File
@@ -48,13 +48,13 @@ const MENU_GROUPS: SidebarMenuGroup[] = [
{
key: "dashboard",
label: "首页",
path: "/dashboard",
path: "/app/dashboard",
icon: <DashboardOutlined />,
},
{
key: "generate",
label: "一键生成",
path: "/generate",
path: "/app/generate",
icon: <VideoCameraOutlined />,
},
],
@@ -65,43 +65,43 @@ const MENU_GROUPS: SidebarMenuGroup[] = [
{
key: "assets",
label: "素材库",
path: "/assets",
path: "/app/assets",
icon: <FileOutlined />,
},
{
key: "voices",
label: "配音库",
path: "/voices",
path: "/app/voices",
icon: <AudioOutlined />,
},
{
key: "voice-clone",
label: "我的音色",
path: "/voice-clone",
path: "/app/voice-clone",
icon: <AudioOutlined />,
},
{
key: "accounts",
label: "账号管理",
path: "/accounts",
path: "/app/accounts",
icon: <AppstoreOutlined />,
},
{
key: "titles",
label: "标题库",
path: "/titles",
path: "/app/titles",
icon: <FileTextOutlined />,
},
{
key: "products",
label: "成片库",
path: "/products",
path: "/app/products",
icon: <TrophyOutlined />,
},
{
key: "templates",
label: "模板库",
path: "/templates",
path: "/app/templates",
icon: <AppstoreOutlined />,
},
],
@@ -112,19 +112,19 @@ const MENU_GROUPS: SidebarMenuGroup[] = [
{
key: "history",
label: "任务历史",
path: "/history",
path: "/app/history",
icon: <HistoryOutlined />,
},
{
key: "admin",
label: "控制台",
path: "/admin",
path: "/app/admin",
icon: <ControlOutlined />,
},
{
key: "subscription",
label: "订阅管理",
path: "/subscription",
path: "/app/subscription",
icon: <CrownOutlined />,
},
],
@@ -133,8 +133,8 @@ const MENU_GROUPS: SidebarMenuGroup[] = [
/** 判断菜单项是否激活 */
const isMenuItemActive = (pathname: string, path: string): boolean => {
if (path === "/dashboard") {
return pathname === "/" || pathname === "/dashboard";
if (path === "/app/dashboard") {
return pathname === "/app" || pathname === "/app/dashboard";
}
return pathname.startsWith(path);
};
+306
View File
@@ -0,0 +1,306 @@
/**
* 首页落地页 — V21 Design System
*
* 5 个区域:HeroSection / FeatureSection / WorkflowSection
* / PricingSection / CTASection
*
* 零 antd 直接导入,全部使用 CSS 变量
*/
import React from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui";
import { useAuthStore } from "@/store/authStore";
import "./home-page.css";
/* ── HeroSection ─────────────────────────────────────────── */
const HeroSection: React.FC = () => {
const navigate = useNavigate();
return (
<section className="hp-hero">
<div className="hp-hero-inner">
{/* 左侧文案 */}
<div className="hp-hero-content">
<span className="hp-hero-tag">🦐 · AI智能视频创作平台</span>
<h1 className="hp-hero-title">
AI自动剪辑
<br />
</h1>
<p className="hp-hero-desc">
AI
30
</p>
<div className="hp-hero-actions">
<Button buttonType="primary" buttonSize="lg" onClick={() => navigate("/register")}>
</Button>
<Button buttonType="secondary" buttonSize="lg" onClick={() => navigate("/pricing")}>
</Button>
</div>
</div>
{/* 右侧视觉 */}
<div className="hp-hero-visual">
<div className="hp-hero-video">
<div className="hp-hero-video-inner">
<span className="hp-hero-video-placeholder">🎬</span>
</div>
<button className="hp-hero-play" type="button" aria-label="播放演示视频">
</button>
</div>
<div className="hp-hero-info">
<div className="hp-hero-badges">
<span className="hp-hero-badge"> AI智能剪辑</span>
<span className="hp-hero-badge"> 30</span>
</div>
<span className="hp-hero-pill"></span>
</div>
</div>
</div>
</section>
);
};
/* ── FeatureSection ──────────────────────────────────────── */
const FEATURES = [
{
icon: "🤖",
title: "AI 智能剪辑",
desc: "自动识别视频高光片段,智能去除冗余内容,一键生成精彩短视频。",
},
{
icon: "🎙️",
title: "AI 配音克隆",
desc: "克隆您的声音,支持多种音色风格,自动生成自然流畅的配音。",
},
{
icon: "📝",
title: "智能字幕标题",
desc: "自动语音识别生成精准字幕,AI 创作吸睛标题,提升内容传播力。",
},
{
icon: "📱",
title: "多平台一键发布",
desc: "支持抖音、快手、小红书、微信视频号等主流平台,一键同步发布。",
},
];
const FeatureSection: React.FC = () => {
return (
<section className="hp-features">
<div className="hp-section-inner">
<h2 className="hp-section-title"></h2>
<p className="hp-section-desc">
AI
</p>
<div className="hp-feature-grid">
{FEATURES.map((f) => (
<div key={f.title} className="hp-feature-card">
<div className="hp-feature-icon">{f.icon}</div>
<h3 className="hp-feature-title">{f.title}</h3>
<p className="hp-feature-desc">{f.desc}</p>
</div>
))}
</div>
</div>
</section>
);
};
/* ── WorkflowSection ─────────────────────────────────────── */
const STEPS = [
{ icon: "📤", title: "上传素材", desc: "拖拽或选择视频素材,支持批量上传" },
{ icon: "🧠", title: "AI 处理", desc: "AI 自动分析、剪辑、配音、加字幕" },
{ icon: "👀", title: "预览调整", desc: "在线预览生成结果,支持微调编辑" },
{ icon: "🚀", title: "一键发布", desc: "多平台同步发布,追踪数据表现" },
];
const WorkflowSection: React.FC = () => {
return (
<section className="hp-workflow">
<div className="hp-section-inner">
<h2 className="hp-section-title"></h2>
<p className="hp-section-desc">
30
</p>
<div className="hp-step-grid">
{STEPS.map((step, idx) => (
<div key={step.title} className="hp-step-card">
<div className="hp-step-number">{idx + 1}</div>
<div className="hp-step-icon">{step.icon}</div>
<h3 className="hp-step-title">{step.title}</h3>
<p className="hp-step-desc">{step.desc}</p>
{idx < STEPS.length - 1 && <div className="hp-step-arrow"></div>}
</div>
))}
</div>
</div>
</section>
);
};
/* ── PricingSection ──────────────────────────────────────── */
const PLANS = [
{
name: "基础版",
price: "免费",
period: "",
desc: "适合个人体验,快速上手",
features: ["每月 5 次 AI 生成", "720p 视频导出", "基础模板库", "1 个平台账号绑定"],
highlighted: false,
cta: "免费开始",
},
{
name: "专业版",
price: "¥99",
period: "/月",
desc: "适合内容创作者,高效产出",
features: [
"每月 100 次 AI 生成",
"1080p 视频导出",
"全部模板库",
"4 个平台账号绑定",
"AI 配音克隆",
"优先客服支持",
],
highlighted: true,
cta: "立即订阅",
},
{
name: "企业版",
price: "¥399",
period: "/月",
desc: "适合团队与企业,规模化运营",
features: [
"无限次 AI 生成",
"4K 视频导出",
"全部模板 + 定制模板",
"无限平台账号绑定",
"团队协作管理",
"API 接入支持",
"专属客户经理",
],
highlighted: false,
cta: "联系销售",
},
];
const PricingSection: React.FC = () => {
const navigate = useNavigate();
return (
<section className="hp-pricing">
<div className="hp-section-inner">
<h2 className="hp-section-title"></h2>
<p className="hp-section-desc"></p>
<div className="hp-pricing-grid">
{PLANS.map((plan) => (
<div
key={plan.name}
className={`hp-pricing-card${plan.highlighted ? " hp-pricing-card--highlight" : ""}`}
>
{plan.highlighted && <div className="hp-pricing-badge"></div>}
<h3 className="hp-pricing-name">{plan.name}</h3>
<div className="hp-pricing-price">
{plan.price}
{plan.period && <span className="hp-pricing-period">{plan.period}</span>}
</div>
<p className="hp-pricing-desc">{plan.desc}</p>
<ul className="hp-pricing-features">
{plan.features.map((f) => (
<li key={f}>
<span className="hp-pricing-check"></span>
{f}
</li>
))}
</ul>
<Button
buttonType={plan.highlighted ? "primary" : "secondary"}
onClick={() => navigate("/register")}
>
{plan.cta}
</Button>
</div>
))}
</div>
</div>
</section>
);
};
/* ── CTASection ──────────────────────────────────────────── */
const CTASection: React.FC = () => {
const navigate = useNavigate();
return (
<section className="hp-cta">
<div className="hp-cta-inner">
<h2 className="hp-cta-title"> AI </h2>
<p className="hp-cta-desc">
AI
</p>
<Button buttonType="primary" buttonSize="lg" onClick={() => navigate("/register")}>
</Button>
</div>
</section>
);
};
/* ── 主页面 ─────────────────────────────────────────────── */
const HomePage: React.FC = () => {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const navigate = useNavigate();
// 已登录用户自动跳转到 dashboard
React.useEffect(() => {
if (isAuthenticated) {
navigate("/app/dashboard", { replace: true });
}
}, [isAuthenticated, navigate]);
return (
<div className="hp-page">
{/* 顶部导航栏 */}
<header className="hp-nav">
<div className="hp-nav-inner">
<button className="hp-nav-brand" type="button" onClick={() => navigate("/")}>
<span className="hp-nav-logo">🦐</span>
<span className="hp-nav-brand-text"></span>
</button>
<div className="hp-nav-actions">
<Button buttonType="ghost" buttonSize="sm" onClick={() => navigate("/login")}>
</Button>
<Button buttonType="primary" buttonSize="sm" onClick={() => navigate("/register")}>
</Button>
</div>
</div>
</header>
{/* 5 个区域 */}
<HeroSection />
<FeatureSection />
<WorkflowSection />
<PricingSection />
<CTASection />
{/* 底部 */}
<footer className="hp-footer">
<p>© 2026 · AI智能视频创作平台</p>
</footer>
</div>
);
};
export default HomePage;
+583
View File
@@ -0,0 +1,583 @@
/**
* 首页落地页 — V21 Design System
*
* 5 个区域:HeroSection / FeatureSection / WorkflowSection
* / PricingSection / CTASection
* 全部使用 CSS 变量,零 antd 直接导入
*/
/* ── 页面容器 ────────────────────────────────────────────── */
.hp-page {
min-height: 100vh;
background: var(--bg-base, #fafafa);
color: var(--text-primary, #1a1a2e);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
overflow-x: hidden;
}
/* ── 顶部导航 ────────────────────────────────────────────── */
.hp-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid var(--line, rgba(0, 0, 0, 0.06));
}
.hp-nav-inner {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
}
.hp-nav-brand {
display: flex;
align-items: center;
gap: 8px;
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.hp-nav-logo {
font-size: 24px;
}
.hp-nav-brand-text {
font-size: 18px;
font-weight: 700;
color: var(--text-primary, #1a1a2e);
}
.hp-nav-actions {
display: flex;
align-items: center;
gap: 12px;
}
/* ── HeroSection ─────────────────────────────────────────── */
.hp-hero {
padding: 120px 24px 60px;
background: linear-gradient(180deg, rgba(99, 102, 241, 0.04) 0%, transparent 60%);
}
.hp-hero-inner {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 420px;
gap: 48px;
align-items: center;
}
.hp-hero-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.hp-hero-tag {
display: inline-flex;
align-self: flex-start;
padding: 6px 16px;
border-radius: var(--radius-sm, 8px);
background: rgba(99, 102, 241, 0.08);
color: var(--primary, #6366f1);
font-size: 14px;
font-weight: 500;
}
.hp-hero-title {
font-size: 44px;
font-weight: 800;
line-height: 1.2;
color: var(--text-primary, #1a1a2e);
margin: 0;
letter-spacing: -0.02em;
}
.hp-hero-desc {
font-size: 17px;
line-height: 1.7;
color: var(--text-secondary, #6b7280);
margin: 0;
max-width: 520px;
}
.hp-hero-actions {
display: flex;
gap: 12px;
margin-top: 8px;
}
/* Hero 右侧视觉 */
.hp-hero-visual {
display: flex;
flex-direction: column;
gap: 16px;
}
.hp-hero-video {
position: relative;
border-radius: var(--radius-lg, 16px);
overflow: hidden;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
aspect-ratio: 16 / 10;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 20px 60px rgba(99, 102, 241, 0.18);
}
.hp-hero-video-inner {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.hp-hero-video-placeholder {
font-size: 64px;
opacity: 0.6;
}
.hp-hero-play {
position: absolute;
width: 56px;
height: 56px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.92);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: var(--primary, #6366f1);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.hp-hero-play:hover {
transform: scale(1.08);
box-shadow: 0 6px 28px rgba(0, 0, 0, 0.2);
}
.hp-hero-info {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.hp-hero-badges {
display: flex;
gap: 8px;
}
.hp-hero-badge {
padding: 6px 14px;
border-radius: var(--radius-sm, 8px);
background: var(--bg-surface, #fff);
border: 1px solid var(--line, rgba(0, 0, 0, 0.06));
font-size: 13px;
font-weight: 500;
color: var(--text-primary, #1a1a2e);
white-space: nowrap;
}
.hp-hero-pill {
padding: 5px 14px;
border-radius: 20px;
background: rgba(16, 185, 129, 0.1);
color: #059669;
font-size: 13px;
font-weight: 600;
}
/* ── 通用 Section 样式 ──────────────────────────────────── */
.hp-section-inner {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
.hp-section-title {
font-size: 32px;
font-weight: 700;
text-align: center;
margin: 0 0 12px;
color: var(--text-primary, #1a1a2e);
}
.hp-section-desc {
font-size: 16px;
text-align: center;
color: var(--text-secondary, #6b7280);
margin: 0 0 48px;
}
/* ── FeatureSection ──────────────────────────────────────── */
.hp-features {
padding: 80px 0;
}
.hp-feature-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
}
.hp-feature-card {
padding: 32px 24px;
border-radius: var(--radius-lg, 16px);
background: var(--bg-surface, #fff);
border: 1px solid var(--line, rgba(0, 0, 0, 0.06));
text-align: center;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.hp-feature-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
}
.hp-feature-icon {
width: 56px;
height: 56px;
margin: 0 auto 16px;
border-radius: var(--radius-md, 12px);
background: rgba(99, 102, 241, 0.08);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
}
.hp-feature-title {
font-size: 17px;
font-weight: 600;
margin: 0 0 8px;
color: var(--text-primary, #1a1a2e);
}
.hp-feature-desc {
font-size: 14px;
line-height: 1.6;
color: var(--text-secondary, #6b7280);
margin: 0;
}
/* ── WorkflowSection ─────────────────────────────────────── */
.hp-workflow {
padding: 80px 0;
background: linear-gradient(180deg, transparent 0%, rgba(99, 102, 241, 0.03) 50%, transparent 100%);
}
.hp-step-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
position: relative;
}
.hp-step-card {
position: relative;
padding: 32px 24px;
border-radius: var(--radius-lg, 16px);
background: var(--bg-surface, #fff);
border: 1px solid var(--line, rgba(0, 0, 0, 0.06));
text-align: center;
}
.hp-step-number {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--primary, #6366f1);
color: #fff;
font-size: 13px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.hp-step-icon {
font-size: 36px;
margin: 8px 0 12px;
}
.hp-step-title {
font-size: 17px;
font-weight: 600;
margin: 0 0 8px;
color: var(--text-primary, #1a1a2e);
}
.hp-step-desc {
font-size: 14px;
line-height: 1.6;
color: var(--text-secondary, #6b7280);
margin: 0;
}
.hp-step-arrow {
position: absolute;
right: -18px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
color: var(--text-secondary, #6b7280);
opacity: 0.4;
z-index: 1;
}
/* ── PricingSection ──────────────────────────────────────── */
.hp-pricing {
padding: 80px 0;
}
.hp-pricing-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
align-items: start;
}
.hp-pricing-card {
position: relative;
padding: 36px 28px;
border-radius: var(--radius-lg, 16px);
background: var(--bg-surface, #fff);
border: 1px solid var(--line, rgba(0, 0, 0, 0.06));
display: flex;
flex-direction: column;
gap: 16px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.hp-pricing-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
}
.hp-pricing-card--highlight {
border-color: var(--primary, #6366f1);
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.12);
}
.hp-pricing-badge {
position: absolute;
top: -12px;
right: 20px;
padding: 4px 14px;
border-radius: 20px;
background: var(--primary, #6366f1);
color: #fff;
font-size: 12px;
font-weight: 600;
}
.hp-pricing-name {
font-size: 20px;
font-weight: 600;
margin: 0;
color: var(--text-primary, #1a1a2e);
}
.hp-pricing-price {
font-size: 36px;
font-weight: 800;
color: var(--text-primary, #1a1a2e);
line-height: 1;
}
.hp-pricing-period {
font-size: 15px;
font-weight: 400;
color: var(--text-secondary, #6b7280);
}
.hp-pricing-desc {
font-size: 14px;
color: var(--text-secondary, #6b7280);
margin: 0;
}
.hp-pricing-features {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.hp-pricing-features li {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--text-primary, #1a1a2e);
}
.hp-pricing-check {
color: #10b981;
font-weight: 700;
font-size: 14px;
}
/* ── CTASection ──────────────────────────────────────────── */
.hp-cta {
padding: 80px 24px;
}
.hp-cta-inner {
max-width: 680px;
margin: 0 auto;
text-align: center;
padding: 60px 40px;
border-radius: var(--radius-lg, 16px);
background: linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(139, 92, 246, 0.06) 100%);
border: 1px solid var(--line, rgba(0, 0, 0, 0.06));
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.hp-cta-title {
font-size: 30px;
font-weight: 700;
margin: 0;
color: var(--text-primary, #1a1a2e);
}
.hp-cta-desc {
font-size: 16px;
line-height: 1.6;
color: var(--text-secondary, #6b7280);
margin: 0;
}
/* ── 底部 ────────────────────────────────────────────────── */
.hp-footer {
padding: 32px 24px;
text-align: center;
border-top: 1px solid var(--line, rgba(0, 0, 0, 0.06));
}
.hp-footer p {
margin: 0;
font-size: 13px;
color: var(--text-secondary, #6b7280);
}
/* ── 响应式 768px ────────────────────────────────────────── */
@media (max-width: 768px) {
.hp-hero-inner {
grid-template-columns: 1fr;
gap: 32px;
}
.hp-hero-visual {
max-width: 400px;
margin: 0 auto;
}
.hp-hero-title {
font-size: 32px;
}
.hp-hero {
padding: 100px 24px 48px;
}
.hp-feature-grid {
grid-template-columns: repeat(2, 1fr);
}
.hp-step-grid {
grid-template-columns: repeat(2, 1fr);
}
.hp-step-arrow {
display: none;
}
.hp-pricing-grid {
grid-template-columns: 1fr;
max-width: 400px;
margin: 0 auto;
}
.hp-section-title {
font-size: 26px;
}
.hp-cta-inner {
padding: 40px 24px;
}
.hp-cta-title {
font-size: 24px;
}
}
/* ── 响应式 480px ────────────────────────────────────────── */
@media (max-width: 480px) {
.hp-hero-title {
font-size: 26px;
}
.hp-hero-desc {
font-size: 15px;
}
.hp-hero-actions {
flex-direction: column;
}
.hp-hero-badges {
flex-direction: column;
gap: 6px;
}
.hp-feature-grid {
grid-template-columns: 1fr;
}
.hp-step-grid {
grid-template-columns: 1fr;
}
.hp-nav-actions {
gap: 8px;
}
}
+19 -2
View File
@@ -9,6 +9,7 @@ import Login from "@/pages/auth/Login";
import Register from "@/pages/auth/Register";
import ForgotPassword from "@/pages/auth/ForgotPassword";
import ResetPassword from "@/pages/auth/ResetPassword";
import HomePage from "@/pages/home/HomePage";
import { useAuthStore } from "@/store/authStore";
/** 受保护的路由组件 */
@@ -24,8 +25,24 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
/** 首页路由组件:已登录跳 dashboard,未登录显示落地页 */
const HomeRoute: React.FC = () => {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const hasAccessToken = Boolean(localStorage.getItem("access_token"));
if (isAuthenticated && hasAccessToken) {
return <Navigate to="/app/dashboard" replace />;
}
return <HomePage />;
};
/** 路由配置 */
export const router = createBrowserRouter([
{
path: "/",
element: <HomeRoute />,
},
{
path: "/login",
element: <Login />,
@@ -43,7 +60,7 @@ export const router = createBrowserRouter([
element: <ResetPassword />,
},
{
path: "/",
path: "/app",
element: (
<ProtectedRoute>
<MainLayout />
@@ -52,7 +69,7 @@ export const router = createBrowserRouter([
children: [
{
index: true,
element: <Navigate to="/dashboard" replace />,
element: <Navigate to="/app/dashboard" replace />,
},
{
path: "dashboard",