feat: redesign market-facing ui surfaces

This commit is contained in:
2026-04-23 14:29:41 +08:00
parent 2e5344618a
commit 1f5c452a30
9 changed files with 1767 additions and 1572 deletions

View File

@@ -47,7 +47,7 @@ export default function DashboardPage() {
<span className="header-kicker">Control overview</span> <span className="header-kicker">Control overview</span>
<h3></h3> <h3></h3>
<p className="muted"> <p className="muted">
MVP 线
</p> </p>
</div> </div>
<div className="dashboard-stage-grid"> <div className="dashboard-stage-grid">
@@ -63,8 +63,8 @@ export default function DashboardPage() {
</div> </div>
<div className="dashboard-mini-card"> <div className="dashboard-mini-card">
<span>线</span> <span>线</span>
<strong> mock / </strong> <strong> / </strong>
<small></small> <small></small>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { startTransition, useState } from "react"; import { startTransition, useState } from "react";
import { ArrowRight, Blocks, Coins, Settings2, ShieldCheck } from "lucide-react"; import { ArrowRight, Blocks, Coins, ShieldCheck } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -31,65 +31,51 @@ export default function AdminLoginPage() {
} }
return ( return (
<div className="login-grid auth-grid-admin"> <div className="admin-auth-page">
<section className="auth-hero auth-hero-admin"> <section className="admin-auth-copy">
<div className="auth-copy"> <span className="admin-page-kicker">OPERATIONS CONSOLE</span>
<div className="brand-kicker">Ops Console</div> <h1>AIVIDEO CONTROL SURFACE</h1>
<h1 className="headline"></h1> <p>
<p className="subcopy"> dark console
沿 `MiniMax-DESIGN.md` </p>
</p>
<div className="auth-chip-row"> <div className="admin-auth-grid">
<span className="auth-chip">Provider routing</span> <article className="admin-auth-info">
<span className="auth-chip">Pricing control</span> <Blocks size={18} />
<span className="auth-chip">Audit trail</span> <strong></strong>
</div> <span></span>
</div> </article>
<div className="showcase-stack"> <article className="admin-auth-info">
<div className="showcase-card showcase-card-hero"> <Coins size={18} />
<div className="toolbar"> <strong></strong>
<span className="status-badge tone-success">Admin access</span> <span></span>
<span className="mini-note">Operational overview</span> </article>
</div> <article className="admin-auth-info">
<h3></h3> <ShieldCheck size={18} />
<p className="muted"> <strong></strong>
MVP <span></span>
</p> </article>
</div>
<div className="showcase-card-grid">
<article className="showcase-card">
<Blocks size={18} />
<strong></strong>
<span></span>
</article>
<article className="showcase-card">
<Coins size={18} />
<strong></strong>
<span></span>
</article>
<article className="showcase-card">
<Settings2 size={18} />
<strong></strong>
<span>便</span>
</article>
</div>
</div> </div>
</section> </section>
<section className="fullscreen-shell"> <section className="admin-auth-panel">
<form className="auth-card auth-card-strong" onSubmit={handleSubmit}> <form className="admin-auth-card" onSubmit={handleSubmit}>
<div className="auth-card-head"> <div className="admin-auth-head">
<div className="brand-kicker">AIVideo Admin</div> <span className="admin-page-kicker">ADMIN ACCESS</span>
<h3></h3> <h2></h2>
<p className="muted"></p> <p></p>
</div> </div>
<div className="form-stack"> <div className="form-stack">
<label className="field-label"> <label className="field-label">
<input <input
value={form.username} value={form.username}
onChange={(event) => onChange={(event) =>
setForm((previous) => ({ ...previous, username: event.target.value })) setForm((previous) => ({
...previous,
username: event.target.value,
}))
} }
/> />
</label> </label>
@@ -99,23 +85,28 @@ export default function AdminLoginPage() {
type="password" type="password"
value={form.password} value={form.password}
onChange={(event) => onChange={(event) =>
setForm((previous) => ({ ...previous, password: event.target.value })) setForm((previous) => ({
...previous,
password: event.target.value,
}))
} }
/> />
</label> </label>
{error ? <div className="inline-feedback is-error">{error}</div> : null} {error ? <div className="inline-feedback is-error">{error}</div> : null}
<button className="primary-button" type="submit"> <button className="primary-button" type="submit">
{loading ? "登录中..." : "进入后台"} {loading ? "SIGNING IN..." : "ENTER CONSOLE"}
{loading ? null : <ArrowRight size={16} />} {loading ? null : <ArrowRight size={16} />}
</button> </button>
<div className="auth-action-row">
<div className="auth-form-actions">
<Link href="/login" className="ghost-button"> <Link href="/login" className="ghost-button">
</Link> </Link>
<div className="field-note"> <Link href="/" className="auth-inline-link">
<ShieldCheck size={14} />
</Link>
</div>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -1,19 +1,16 @@
import { DM_Sans, Outfit, Roboto_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
export const displayFont = Outfit({ export const displayFont = Geist({
subsets: ["latin"], subsets: ["latin"],
variable: "--font-display", variable: "--font-display",
weight: ["500", "600", "700"],
}); });
export const bodyFont = DM_Sans({ export const bodyFont = Geist({
subsets: ["latin"], subsets: ["latin"],
variable: "--font-body", variable: "--font-body",
weight: ["400", "500", "700"],
}); });
export const monoFont = Roboto_Mono({ export const monoFont = Geist_Mono({
subsets: ["latin"], subsets: ["latin"],
variable: "--font-mono", variable: "--font-mono",
weight: ["400", "500", "600"],
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { startTransition, useState } from "react"; import { startTransition, useState } from "react";
import { ArrowRight, Clapperboard, Coins, FolderKanban, Sparkles } from "lucide-react"; import { ArrowRight, Coins, FolderKanban, ImagePlus } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -9,7 +9,10 @@ import { api, ApiError } from "@/lib/api";
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
const [form, setForm] = useState({ account: "demo@example.com", password: "12345678" }); const [form, setForm] = useState({
account: "demo@example.com",
password: "12345678",
});
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -28,65 +31,54 @@ export default function LoginPage() {
} }
return ( return (
<div className="auth-grid"> <div className="auth-page auth-page-user">
<section className="auth-hero auth-hero-studio"> <section className="auth-showcase auth-showcase-user">
<div className="auth-copy"> <div className="auth-showcase-copy">
<div className="eyebrow">Creative Workbench</div> <span className="auth-showcase-kicker">AIVIDEO FOR CREATORS</span>
<h1 className="headline"> AI 线</h1> <h1></h1>
<p className="subcopy"> <p>
`MiniMax-DESIGN.md` AIVideo
</p> </p>
<div className="auth-chip-row">
<span className="auth-chip">Prompt to video</span>
<span className="auth-chip">Capability cards</span>
<span className="auth-chip">Realtime task polling</span>
</div>
</div> </div>
<div className="showcase-stack">
<div className="showcase-card showcase-card-hero"> <div className="auth-showcase-grid">
<div className="toolbar"> <article className="auth-showcase-card">
<span className="status-badge tone-success">Studio ready</span> <ImagePlus size={18} />
<span className="mini-note">Source mode</span> <strong></strong>
</div> <span></span>
<h3></h3> </article>
<p className="muted"> <article className="auth-showcase-card">
mock SQLite <FolderKanban size={18} />
</p> <strong></strong>
</div> <span></span>
<div className="showcase-card-grid"> </article>
<article className="showcase-card"> <article className="auth-showcase-card">
<Clapperboard size={18} /> <Coins size={18} />
<strong></strong> <strong></strong>
<span></span> <span></span>
</article> </article>
<article className="showcase-card">
<FolderKanban size={18} />
<strong></strong>
<span></span>
</article>
<article className="showcase-card">
<Coins size={18} />
<strong></strong>
<span></span>
</article>
</div>
</div> </div>
</section> </section>
<section className="fullscreen-shell"> <section className="auth-form-column">
<form className="auth-card auth-card-strong" onSubmit={handleSubmit}> <form className="auth-form-card" onSubmit={handleSubmit}>
<div className="auth-card-head"> <div className="auth-form-head">
<div className="eyebrow">Welcome Back</div> <span className="auth-form-kicker">Welcome Back</span>
<h2> AIVideo</h2> <h2> AIVideo</h2>
<p className="muted"></p> <p></p>
</div> </div>
<div className="form-stack"> <div className="form-stack">
<label className="field-label"> <label className="field-label">
<input <input
value={form.account} value={form.account}
onChange={(event) => onChange={(event) =>
setForm((previous) => ({ ...previous, account: event.target.value })) setForm((previous) => ({
...previous,
account: event.target.value,
}))
} }
/> />
</label> </label>
@@ -96,23 +88,28 @@ export default function LoginPage() {
type="password" type="password"
value={form.password} value={form.password}
onChange={(event) => onChange={(event) =>
setForm((previous) => ({ ...previous, password: event.target.value })) setForm((previous) => ({
...previous,
password: event.target.value,
}))
} }
/> />
</label> </label>
{error ? <div className="inline-feedback is-error">{error}</div> : null} {error ? <div className="inline-feedback is-error">{error}</div> : null}
<button className="primary-button" disabled={loading} type="submit"> <button className="primary-button" disabled={loading} type="submit">
{loading ? "登录中..." : "登录并进入工作台"} {loading ? "登录中..." : "进入创作工作台"}
{loading ? null : <ArrowRight size={16} />} {loading ? null : <ArrowRight size={16} />}
</button> </button>
<div className="auth-action-row">
<div className="auth-form-actions">
<Link href="/register" className="ghost-button"> <Link href="/register" className="ghost-button">
</Link> </Link>
<div className="field-note"> <Link href="/" className="auth-inline-link">
<Sparkles size={14} />
</Link>
</div>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -1,6 +1,189 @@
import { redirect } from "next/navigation"; import {
ArrowRight,
CirclePlay,
Coins,
FolderKanban,
ImagePlus,
ShieldCheck,
Sparkles,
} from "lucide-react";
import Link from "next/link";
const productHighlights = [
{
title: "生成工作台",
copy: "把模型、提示词、时长、比例和素材引用收进一个创作面板。",
icon: Sparkles,
},
{
title: "素材系统",
copy: "图片、视频、音频参考素材统一入库,创作时直接调用。",
icon: ImagePlus,
},
{
title: "任务追踪",
copy: "生成状态实时刷新,结果视频、积分消耗和任务详情一屏可见。",
icon: FolderKanban,
},
{
title: "积分与增长",
copy: "充值、兑换码、注册奖励和邀请关系形成完整的商业闭环。",
icon: Coins,
},
];
export default function HomePage() { export default function HomePage() {
redirect("/workspace/create"); return (
} <main className="marketing-home">
<header className="marketing-nav">
<div className="marketing-logo">AIVideo</div>
<nav className="marketing-nav-links">
<a href="#product"></a>
<a href="#workflow"></a>
<a href="#ops"></a>
</nav>
<div className="marketing-nav-actions">
<Link href="/login" className="marketing-link-button">
</Link>
<Link href="/register" className="marketing-primary-button">
</Link>
</div>
</header>
<section className="marketing-hero">
<div className="marketing-hero-copy">
<span className="marketing-kicker">AI VIDEO PLATFORM</span>
<h1></h1>
<p>
AIVideo
</p>
<div className="marketing-hero-actions">
<Link href="/register" className="marketing-primary-button">
<ArrowRight size={16} />
</Link>
<Link href="/login" className="marketing-secondary-button">
</Link>
</div>
<div className="marketing-stat-row">
<div>
<strong></strong>
<span></span>
</div>
<div>
<strong></strong>
<span></span>
</div>
<div>
<strong></strong>
<span></span>
</div>
</div>
</div>
<div className="marketing-stage">
<div className="marketing-stage-card marketing-stage-card-dark">
<div className="marketing-stage-label">Creator Workspace</div>
<h2> AI </h2>
<p>
</p>
<div className="marketing-chip-row">
<span>Text to video</span>
<span>Asset references</span>
<span>Task polling</span>
</div>
</div>
<div className="marketing-stage-grid">
<div className="marketing-stage-card">
<CirclePlay size={18} />
<strong></strong>
<span></span>
</div>
<div className="marketing-stage-card">
<Coins size={18} />
<strong></strong>
<span></span>
</div>
</div>
</div>
</section>
<section className="marketing-section marketing-section-light" id="product">
<div className="marketing-section-copy">
<span className="marketing-kicker">Product Capabilities</span>
<h2></h2>
<p>
DEMO
</p>
</div>
<div className="marketing-tile-grid">
{productHighlights.map((item) => {
const Icon = item.icon;
return (
<article className="marketing-tile" key={item.title}>
<div className="marketing-tile-icon">
<Icon size={20} />
</div>
<h3>{item.title}</h3>
<p>{item.copy}</p>
</article>
);
})}
</div>
</section>
<section className="marketing-section marketing-section-dark" id="workflow">
<div className="marketing-section-copy">
<span className="marketing-kicker">Workflow</span>
<h2></h2>
</div>
<div className="marketing-flow-grid">
<div className="marketing-flow-card">
<span>01</span>
<strong></strong>
<p></p>
</div>
<div className="marketing-flow-card">
<span>02</span>
<strong></strong>
<p></p>
</div>
<div className="marketing-flow-card">
<span>03</span>
<strong></strong>
<p></p>
</div>
</div>
</section>
<section className="marketing-section marketing-section-light" id="ops">
<div className="marketing-ops-shell">
<div className="marketing-section-copy">
<span className="marketing-kicker">Operations Console</span>
<h2></h2>
<p>
</p>
</div>
<div className="marketing-ops-card">
<ShieldCheck size={20} />
<strong></strong>
<p>
AIVideo
</p>
<Link href="/admin/login" className="marketing-link-inline">
<ArrowRight size={14} />
</Link>
</div>
</div>
</section>
</main>
);
}

View File

@@ -21,20 +21,20 @@ import { useEffect, useMemo } from "react";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
const navigation = [ const navigation = [
{ href: "/admin/dashboard", label: "仪表盘", description: "查看系统总览、支付与任务健康度", icon: ChartColumnBig }, { href: "/admin/dashboard", label: "OVERVIEW", title: "系统总览", description: "查看平台总览、支付与任务健康度", icon: ChartColumnBig },
{ href: "/admin/users", label: "用户管理", description: "维护用户状态、余额与基本资料", icon: Users }, { href: "/admin/users", label: "USERS", title: "用户管理", description: "维护用户状态、余额与资料", icon: Users },
{ href: "/admin/recharge-orders", label: "充值订单", description: "跟踪充值流转与支付状态", icon: Coins }, { href: "/admin/recharge-orders", label: "ORDERS", title: "充值订单", description: "跟踪充值流转与支付状态", icon: Coins },
{ href: "/admin/redeem-codes", label: "兑换密钥", description: "批量生成、停用审计兑换码", icon: KeySquare }, { href: "/admin/redeem-codes", label: "REDEEM", title: "兑换密钥", description: "管理兑换码的创建、停用审计", icon: KeySquare },
{ href: "/admin/growth-rules", label: "增长奖励", description: "配置注册奖励与邀请返利规则", icon: Link2 }, { href: "/admin/growth-rules", label: "GROWTH", title: "增长奖励", description: "配置注册奖励与邀请返利规则", icon: Link2 },
{ href: "/admin/invite-relations", label: "邀请关系", description: "查看邀请码链路与归因结果", icon: Link2 }, { href: "/admin/invite-relations", label: "INVITES", title: "邀请关系", description: "查看邀请码链路与归因结果", icon: Link2 },
{ href: "/admin/provider-accounts", label: "供应商账号", description: "管理供应商 baseUrl 与鉴权信息", icon: Workflow }, { href: "/admin/provider-accounts", label: "ACCOUNTS", title: "供应商账号", description: "管理供应商账号和鉴权信息", icon: Workflow },
{ href: "/admin/provider-models", label: "供应商模型", description: "维护供应商模型能力与协议", icon: Blocks }, { href: "/admin/provider-models", label: "MODELS", title: "供应商模型", description: "维护供应商模型能力与协议", icon: Blocks },
{ href: "/admin/video-models", label: "平台模型", description: "定义前台可见的统一模型层", icon: Package2 }, { href: "/admin/video-models", label: "CATALOG", title: "平台模型", description: "定义前台可见的统一模型层", icon: Package2 },
{ href: "/admin/video-model-bindings", label: "模型绑定", description: "决定平台模型到供应商模型的路由", icon: Workflow }, { href: "/admin/video-model-bindings", label: "ROUTING", title: "模型绑定", description: "决定平台模型到供应商模型的路由", icon: Workflow },
{ href: "/admin/pricing-rules", label: "价格规则", description: "按模型与版本维护积分定价", icon: Coins }, { href: "/admin/pricing-rules", label: "PRICING", title: "价格规则", description: "按模型与版本维护积分定价", icon: Coins },
{ href: "/admin/video-tasks", label: "视频任务", description: "处理异常任务与追踪回调结果", icon: Workflow }, { href: "/admin/video-tasks", label: "TASKS", title: "视频任务", description: "处理异常任务与追踪执行结果", icon: Workflow },
{ href: "/admin/callback-logs", label: "回调日志", description: "查看供应商回调负载与重放线索", icon: ChartColumnBig }, { href: "/admin/callback-logs", label: "CALLBACKS", title: "回调日志", description: "查看供应商回调负载与异常线索", icon: ChartColumnBig },
{ href: "/admin/system-config", label: "系统配置", description: "维护站点公告、策略与公开配置", icon: Settings2 }, { href: "/admin/system-config", label: "CONFIG", title: "系统配置", description: "维护站点和策略配置", icon: Settings2 },
]; ];
export function AdminShell({ children }: { children: React.ReactNode }) { export function AdminShell({ children }: { children: React.ReactNode }) {
@@ -61,119 +61,71 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
if (meQuery.isLoading || !meQuery.data) { if (meQuery.isLoading || !meQuery.data) {
return ( return (
<div className="fullscreen-shell"> <div className="fullscreen-shell admin-loading-shell">
<div className="pulse-card">...</div> <div className="pulse-card">...</div>
</div> </div>
); );
} }
return ( return (
<div className="shell-grid shell-grid-admin"> <div className="admin-app-shell">
<aside className="shell-sidebar"> <aside className="admin-rail">
<div className="brand-block brand-block-admin"> <div className="admin-rail-brand">
<div className="brand-visual" aria-hidden="true"> <span className="admin-rail-kicker">AIVIDEO CONTROL</span>
<span className="brand-orb brand-orb-primary" /> <h1>AIVideo</h1>
<span className="brand-orb brand-orb-secondary" /> <p></p>
</div>
<span className="brand-kicker">AIVIDEO ADMIN</span>
<h1></h1>
<p></p>
<div className="brand-pill-row">
<span className="brand-pill">Ops control</span>
<span className="brand-pill">Pricing</span>
<span className="brand-pill">Capability routing</span>
</div>
</div> </div>
<div className="sidebar-note"> <nav className="admin-rail-nav">
<div className="sidebar-note-label">Operations Matrix</div> {navigation.map((item) => {
<div className="sidebar-metrics">
<div className="sidebar-metric">
<strong>Models</strong>
<span></span>
</div>
<div className="sidebar-metric">
<strong>Economy</strong>
<span></span>
</div>
<div className="sidebar-metric">
<strong>Audit</strong>
<span></span>
</div>
</div>
</div>
<nav className="sidebar-nav">
{navigation.map((item, index) => {
const Icon = item.icon; const Icon = item.icon;
return ( return (
<Link <Link
key={item.href} key={item.href}
href={item.href} href={item.href}
className={clsx("nav-item", { className={clsx("admin-rail-link", {
active: pathname.startsWith(item.href), active: pathname.startsWith(item.href),
})} })}
> >
<Icon size={18} /> <Icon size={15} />
<div> <span>{item.label}</span>
<span>{item.label}</span>
<small>{item.description}</small>
</div>
<div className="nav-index">{String(index + 1).padStart(2, "0")}</div>
</Link> </Link>
); );
})} })}
</nav> </nav>
<div className="profile-card"> <div className="admin-rail-footer">
<div className="profile-card-main"> <div className="admin-rail-user">
<div className="profile-avatar"> <strong>{(meQuery.data as { nickname: string }).nickname}</strong>
{String((meQuery.data as { nickname: string }).nickname ?? "AD") <span>{(meQuery.data as { username: string }).username}</span>
.slice(0, 2)
.toUpperCase()}
</div>
<div className="profile-name">
{(meQuery.data as { nickname: string }).nickname}
</div>
<div className="profile-meta">
{(meQuery.data as { username: string }).username}
</div>
</div>
<div className="profile-card-stats">
<div className="profile-stat">
<span></span>
<strong>Admin</strong>
</div>
<div className="profile-stat">
<span></span>
<strong>Control</strong>
</div>
</div> </div>
<button <button
className="ghost-button" className="ghost-button compact-button"
onClick={async () => { onClick={async () => {
await api.post("/api/v1/admin/auth/logout"); await api.post("/api/v1/admin/auth/logout");
router.replace("/admin/login"); router.replace("/admin/login");
}} }}
type="button"
> >
<LogOut size={16} /> <LogOut size={14} />
退 SIGN OUT
</button> </button>
</div> </div>
</aside> </aside>
<main className="shell-main"> <main className="admin-main-shell">
<header className="shell-header"> <header className="admin-page-hero">
<div className="shell-header-content"> <div>
<div className="header-kicker">Operations Console</div> <span className="admin-page-kicker">OPERATIONS CONSOLE</span>
<h2>{current.label}</h2> <h2>{current.title}</h2>
<p className="shell-header-copy">{current.description}</p> <p>{current.description}</p>
</div> </div>
<div className="shell-header-meta"> <div className="admin-page-tags">
<div className="header-chip header-chip-success"></div> <span>LIVE SESSION</span>
<div className="header-chip"> / / </div> <span>RULES / TASKS / PRICING</span>
</div> </div>
</header> </header>
<section className="shell-content">{children}</section> <section className="shell-content">{children}</section>
</main> </main>
</div> </div>

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { startTransition, useState } from "react"; import { startTransition, useState } from "react";
import { ArrowRight, Coins, Gift, Link2, Sparkles } from "lucide-react"; import { ArrowRight, Coins, Gift, Link2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -36,58 +36,43 @@ export function RegisterForm({ inviteCode = "" }: { inviteCode?: string }) {
} }
return ( return (
<div className="auth-grid"> <div className="auth-page auth-page-user">
<section className="auth-hero auth-hero-studio"> <section className="auth-showcase auth-showcase-user">
<div className="auth-copy"> <div className="auth-showcase-copy">
<div className="eyebrow">New Account</div> <span className="auth-showcase-kicker">CREATE ACCOUNT</span>
<h1 className="headline"></h1> <h1></h1>
<p className="subcopy"> <p>
</p> </p>
<div className="auth-chip-row">
<span className="auth-chip">Signup rewards</span>
<span className="auth-chip">Invite tracking</span>
<span className="auth-chip">Wallet ready</span>
</div>
</div> </div>
<div className="showcase-stack">
<div className="showcase-card showcase-card-hero"> <div className="auth-showcase-grid">
<div className="toolbar"> <article className="auth-showcase-card">
<span className="status-badge tone-soft">Growth loop</span> <Gift size={18} />
<span className="mini-note">Reward rules active</span> <strong></strong>
</div> <span></span>
<h3></h3> </article>
<p className="muted"> <article className="auth-showcase-card">
<Link2 size={18} />
</p> <strong></strong>
</div> <span></span>
<div className="showcase-card-grid"> </article>
<article className="showcase-card"> <article className="auth-showcase-card">
<Gift size={18} /> <Coins size={18} />
<strong></strong> <strong></strong>
<span></span> <span></span>
</article> </article>
<article className="showcase-card">
<Link2 size={18} />
<strong></strong>
<span></span>
</article>
<article className="showcase-card">
<Coins size={18} />
<strong></strong>
<span></span>
</article>
</div>
</div> </div>
</section> </section>
<section className="fullscreen-shell"> <section className="auth-form-column">
<form className="auth-card auth-card-strong" onSubmit={handleSubmit}> <form className="auth-form-card" onSubmit={handleSubmit}>
<div className="auth-card-head"> <div className="auth-form-head">
<div className="eyebrow">Create Account</div> <span className="auth-form-kicker">Get Started</span>
<h2></h2> <h2></h2>
<p className="muted"></p> <p></p>
</div> </div>
<div className="form-stack"> <div className="form-stack">
<label className="field-label"> <label className="field-label">
@@ -95,7 +80,10 @@ export function RegisterForm({ inviteCode = "" }: { inviteCode?: string }) {
placeholder="user@example.com" placeholder="user@example.com"
value={form.account} value={form.account}
onChange={(event) => onChange={(event) =>
setForm((previous) => ({ ...previous, account: event.target.value })) setForm((previous) => ({
...previous,
account: event.target.value,
}))
} }
/> />
</label> </label>
@@ -105,7 +93,10 @@ export function RegisterForm({ inviteCode = "" }: { inviteCode?: string }) {
type="password" type="password"
value={form.password} value={form.password}
onChange={(event) => onChange={(event) =>
setForm((previous) => ({ ...previous, password: event.target.value })) setForm((previous) => ({
...previous,
password: event.target.value,
}))
} }
/> />
</label> </label>
@@ -114,23 +105,28 @@ export function RegisterForm({ inviteCode = "" }: { inviteCode?: string }) {
<input <input
value={form.inviteCode} value={form.inviteCode}
onChange={(event) => onChange={(event) =>
setForm((previous) => ({ ...previous, inviteCode: event.target.value })) setForm((previous) => ({
...previous,
inviteCode: event.target.value,
}))
} }
/> />
</label> </label>
{error ? <div className="inline-feedback is-error">{error}</div> : null} {error ? <div className="inline-feedback is-error">{error}</div> : null}
<button className="primary-button" disabled={loading} type="submit"> <button className="primary-button" disabled={loading} type="submit">
{loading ? "注册中..." : "注册并领取初始积分"} {loading ? "注册中..." : "注册并进入工作台"}
{loading ? null : <ArrowRight size={16} />} {loading ? null : <ArrowRight size={16} />}
</button> </button>
<div className="auth-action-row">
<div className="auth-form-actions">
<Link href="/login" className="ghost-button"> <Link href="/login" className="ghost-button">
</Link> </Link>
<div className="field-note"> <Link href="/" className="auth-inline-link">
<Sparkles size={14} />
</Link>
</div>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -2,17 +2,7 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import clsx from "clsx"; import clsx from "clsx";
import { import { Activity, LogOut } from "lucide-react";
Activity,
Coins,
FolderKanban,
ImagePlus,
LogOut,
Sparkles,
Ticket,
UserRound,
Users,
} from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
@@ -23,51 +13,39 @@ import type { UserProfile } from "@/lib/types";
const navigation = [ const navigation = [
{ {
href: "/workspace/create", href: "/workspace/create",
label: "工作台", label: "创作",
description: "生成参数、提示词与素材联动", title: "生成工作台",
icon: Sparkles, description: "组织模型、提示词、素材和任务提交。",
}, },
{ {
href: "/workspace/tasks", href: "/workspace/tasks",
label: "任务记录", label: "任务",
description: "查看队列、状态轮询与结果视频", title: "任务记录",
icon: FolderKanban, description: "追踪生成状态、扣费结果和最终视频。",
}, },
{ {
href: "/workspace/assets", href: "/workspace/assets",
label: "素材管理", label: "素材",
description: "上传图片、视频和音频素材", title: "素材管理",
icon: ImagePlus, description: "统一管理图片、视频和音频参考素材。",
}, },
{ {
href: "/wallet", href: "/wallet",
label: "钱包概览", label: "钱包",
description: "查看可用积分和冻结明细", title: "钱包概览",
icon: Coins, description: "查看积分、充值和兑换的资金流转。",
},
{
href: "/wallet/recharge",
label: "充值中心",
description: "采购积分,补足生产额度",
icon: Coins,
},
{
href: "/wallet/redeem",
label: "兑换密钥",
description: "使用兑换码快速充值",
icon: Ticket,
}, },
{ {
href: "/invite", href: "/invite",
label: "邀请中心", label: "邀请",
description: "管理关系链与推广奖励", title: "邀请中心",
icon: Users, description: "处理邀请关系和增长奖励链路。",
}, },
{ {
href: "/profile", href: "/profile",
label: "个人资料", label: "账户",
description: "维护账户信息与展示名", title: "个人资料",
icon: UserRound, description: "维护基础资料和账户身份信息。",
}, },
]; ];
@@ -89,11 +67,12 @@ export function SiteShell({ children }: { children: React.ReactNode }) {
} }
}, [error, router]); }, [error, router]);
const title = useMemo(() => { const current = useMemo(() => {
const current = [...navigation] return (
.sort((left, right) => right.href.length - left.href.length) [...navigation]
.find((item) => matchesPath(pathname, item.href)); .sort((left, right) => right.href.length - left.href.length)
return current ?? navigation[0]; .find((item) => matchesPath(pathname, item.href)) ?? navigation[0]
);
}, [pathname]); }, [pathname]);
if (isLoading || !data) { if (isLoading || !data) {
@@ -105,116 +84,79 @@ export function SiteShell({ children }: { children: React.ReactNode }) {
} }
return ( return (
<div className="shell-grid shell-grid-studio"> <div className="site-app-shell">
<aside className="shell-sidebar"> <header className="site-topbar">
<div className="brand-block brand-block-studio"> <div className="site-brand-block">
<div className="brand-visual" aria-hidden="true"> <Link className="site-brand" href="/">
<span className="brand-orb brand-orb-primary" /> AIVideo
<span className="brand-orb brand-orb-secondary" /> </Link>
</div> <span className="site-brand-subtitle">AI Video Platform</span>
<span className="brand-kicker">AI VIDEO STUDIO</span>
<h1>AIVideo</h1>
<p> AI </p>
<div className="brand-pill-row">
<span className="brand-pill">Creative matrix</span>
<span className="brand-pill">Mock providers</span>
<span className="brand-pill">Local assets</span>
</div>
<div className="brand-status">
<span className="status-dot" />
线
</div>
</div> </div>
<div className="sidebar-note"> <nav className="site-topnav">
<div className="sidebar-note-label">Capability Grid</div> {navigation.map((item) => (
<div className="sidebar-metrics"> <Link
<div className="sidebar-metric"> key={item.href}
<strong>Prompt</strong> href={item.href}
<span></span> className={clsx("site-topnav-link", {
</div> active: current.href === item.href,
<div className="sidebar-metric"> })}
<strong>Assets</strong> >
<span></span> {item.label}
</div> </Link>
<div className="sidebar-metric"> ))}
<strong>Tasks</strong>
<span></span>
</div>
</div>
</div>
<nav className="sidebar-nav">
{navigation.map((item, index) => {
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={clsx("nav-item", {
active: title.href === item.href,
})}
>
<Icon size={18} />
<div>
<span>{item.label}</span>
<small>{item.description}</small>
</div>
<div className="nav-index">{String(index + 1).padStart(2, "0")}</div>
</Link>
);
})}
</nav> </nav>
<div className="profile-card"> <div className="site-userbar">
<div className="profile-card-main"> <div className="site-userbar-meta">
<div className="profile-avatar"> <strong>{data.nickname || data.username}</strong>
{(data.nickname || data.username || "AI") <span>{data.email}</span>
.slice(0, 2)
.toUpperCase()}
</div>
<div className="profile-name">{data.nickname || data.username}</div>
<div className="profile-meta">{data.email}</div>
</div>
<div className="profile-card-stats">
<div className="profile-stat">
<span></span>
<strong>Creator</strong>
</div>
<div className="profile-stat">
<span>ID</span>
<strong>{(data.publicId || "AIVIDEO").slice(-8)}</strong>
</div>
</div> </div>
<button <button
className="ghost-button" className="ghost-button compact-button"
onClick={async () => { onClick={async () => {
await api.post("/api/v1/auth/logout"); await api.post("/api/v1/auth/logout");
router.replace("/login"); router.replace("/login");
}} }}
type="button"
> >
<LogOut size={16} /> <LogOut size={14} />
退 退
</button> </button>
</div> </div>
</aside> </header>
<main className="shell-main"> <main className="site-main-shell">
<header className="shell-header"> <section className="site-page-hero">
<div className="shell-header-content"> <div className="site-page-copy">
<div className="header-kicker">Creative Ops Console</div> <span className="header-kicker">Creative Workspace</span>
<h2>{title.label}</h2> <h1>{current.title}</h1>
<p className="shell-header-copy">{title.description}</p> <p>{current.description}</p>
</div> </div>
<div className="shell-header-meta"> <div className="site-page-meta">
<div className="header-chip header-chip-success"> <div className="header-chip header-chip-success">
<Activity size={14} /> <Activity size={14} />
</div> </div>
<div className="header-chip">{data.publicId || "UNAVAILABLE"}</div> <div className="header-chip">{data.publicId || "USER"}</div>
<div className="header-chip"> / / mock </div>
</div> </div>
</header> </section>
<section className="site-highlight-strip">
<div className="site-highlight-card">
<span></span>
<strong></strong>
</div>
<div className="site-highlight-card">
<span></span>
<strong></strong>
</div>
<div className="site-highlight-card">
<span></span>
<strong></strong>
</div>
</section>
<section className="shell-content">{children}</section> <section className="shell-content">{children}</section>
</main> </main>
</div> </div>