feat: refresh frontend visual design
This commit is contained in:
@@ -355,10 +355,9 @@ export default function CreateTaskPage() {
|
||||
<div className="workbench-header">
|
||||
<div>
|
||||
<span className="header-kicker">AI Video Studio</span>
|
||||
<h3>工作台</h3>
|
||||
<h3>生成工作台</h3>
|
||||
<p className="muted">
|
||||
按模型选型、提示词、素材引用的顺序完成一次提交。用户侧不暴露
|
||||
URL 之类的平台配置,只保留创作需要的参数。
|
||||
以“模型选型 → 提示词 → 素材引用 → 任务提交”的顺序完成一次创作。平台连接信息被隐藏,只保留真正影响出片的参数。
|
||||
</p>
|
||||
</div>
|
||||
<div className="row">
|
||||
@@ -376,6 +375,27 @@ export default function CreateTaskPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="studio-command-bar">
|
||||
<div className="studio-command-pill">
|
||||
当前模型
|
||||
<strong>{selectedModel?.frontendTitle ?? "等待加载"}</strong>
|
||||
</div>
|
||||
<div className="studio-command-pill">
|
||||
出片规格
|
||||
<strong>
|
||||
{activeDuration}s · {activeResolution} · {activeRatio}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="studio-command-pill">
|
||||
任务预估
|
||||
<strong>{estimatedPoints} 积分</strong>
|
||||
</div>
|
||||
<div className="studio-command-pill">
|
||||
素材引用
|
||||
<strong>{selectedAssetCount} 项</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="studio-summary-grid">
|
||||
<article className="studio-stat">
|
||||
<span>可用积分</span>
|
||||
@@ -644,7 +664,7 @@ export default function CreateTaskPage() {
|
||||
<div>
|
||||
<span className="header-kicker">Asset Dock</span>
|
||||
<h3>素材库</h3>
|
||||
<p className="muted">上传后即时入库,右侧挑素材,左侧直接组成任务。</p>
|
||||
<p className="muted">上传后即时入库,边看缩略图边做引用,创作参数和素材选择留在同一画面里。</p>
|
||||
</div>
|
||||
<div className="workspace-tabs">
|
||||
<span className="tab-chip active">素材库</span>
|
||||
|
||||
@@ -15,6 +15,13 @@ export default function TasksPage() {
|
||||
refetchInterval: 4_000,
|
||||
});
|
||||
|
||||
const tasks = tasksQuery.data ?? [];
|
||||
const runningCount = tasks.filter((task) =>
|
||||
["queued", "submitted", "running"].includes(task.taskStatus),
|
||||
).length;
|
||||
const succeededCount = tasks.filter((task) => task.taskStatus === "succeeded").length;
|
||||
const failedCount = tasks.filter((task) => task.taskStatus === "failed").length;
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<div className="toolbar">
|
||||
@@ -27,13 +34,36 @@ export default function TasksPage() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="task-stat-band">
|
||||
<article className="task-stat-card">
|
||||
<span>任务总数</span>
|
||||
<strong>{tasks.length}</strong>
|
||||
<small>当前账号已提交的所有视频任务</small>
|
||||
</article>
|
||||
<article className="task-stat-card">
|
||||
<span>处理中</span>
|
||||
<strong>{runningCount}</strong>
|
||||
<small>包含排队、已提交和运行中的任务</small>
|
||||
</article>
|
||||
<article className="task-stat-card">
|
||||
<span>已完成</span>
|
||||
<strong>{succeededCount}</strong>
|
||||
<small>完成后可直接打开结果视频</small>
|
||||
</article>
|
||||
<article className="task-stat-card">
|
||||
<span>失败/异常</span>
|
||||
<strong>{failedCount}</strong>
|
||||
<small>建议进入详情页查看供应商回执与报错</small>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div className="list-grid" style={{ marginTop: 18 }}>
|
||||
{tasksQuery.data?.map((task) => (
|
||||
<div className="list-item" key={task.taskNo}>
|
||||
{tasks.map((task) => (
|
||||
<article className="list-item task-stream-card" key={task.taskNo}>
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<strong>{task.taskNo}</strong>
|
||||
<div className="muted">
|
||||
<div className="muted task-stream-meta">
|
||||
{dayjs(task.createdAt).format("YYYY-MM-DD HH:mm:ss")} · {task.durationSeconds}
|
||||
s
|
||||
</div>
|
||||
@@ -59,15 +89,19 @@ export default function TasksPage() {
|
||||
查看详情
|
||||
</Link>
|
||||
{task.resultVideoUrl ? (
|
||||
<a className="primary-button" href={task.resultVideoUrl} target="_blank">
|
||||
<a
|
||||
className="primary-button"
|
||||
href={task.resultVideoUrl}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
打开视频
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,26 +17,118 @@ export default function DashboardPage() {
|
||||
});
|
||||
|
||||
const data = dashboardQuery.data;
|
||||
const summaryCards = [
|
||||
{
|
||||
label: "用户总数",
|
||||
value: data?.users ?? 0,
|
||||
copy: "平台已创建账户数",
|
||||
},
|
||||
{
|
||||
label: "已支付订单",
|
||||
value: data?.paidOrders ?? 0,
|
||||
copy: "已完成支付的充值订单",
|
||||
},
|
||||
{
|
||||
label: "任务总数",
|
||||
value: data?.tasks ?? 0,
|
||||
copy: "全链路视频任务累计数量",
|
||||
},
|
||||
{
|
||||
label: "成功率",
|
||||
value: `${data?.successRate ?? 0}%`,
|
||||
copy: "基于任务完成状态的成功占比",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="stats-grid">
|
||||
<article className="stat-card">
|
||||
<h3>用户总数</h3>
|
||||
<div className="value">{data?.users ?? 0}</div>
|
||||
</article>
|
||||
<article className="stat-card">
|
||||
<h3>已支付订单</h3>
|
||||
<div className="value">{data?.paidOrders ?? 0}</div>
|
||||
</article>
|
||||
<article className="stat-card">
|
||||
<h3>任务总数</h3>
|
||||
<div className="value">{data?.tasks ?? 0}</div>
|
||||
</article>
|
||||
<article className="stat-card">
|
||||
<h3>成功率</h3>
|
||||
<div className="value">{data?.successRate ?? 0}%</div>
|
||||
</article>
|
||||
</section>
|
||||
<div className="admin-dashboard-stack">
|
||||
<section className="panel dashboard-stage">
|
||||
<div className="dashboard-stage-copy">
|
||||
<span className="header-kicker">Control overview</span>
|
||||
<h3>统一观察用户经济、模型路由和任务健康度</h3>
|
||||
<p className="muted">
|
||||
当前后台适合快速联调 MVP:前台创作流、积分系统、邀请奖励、模型供应商和任务轮询都能在这里找到对应控制面。
|
||||
</p>
|
||||
</div>
|
||||
<div className="dashboard-stage-grid">
|
||||
<div className="dashboard-mini-card">
|
||||
<span>业务主线</span>
|
||||
<strong>用户 + 任务 + 钱包</strong>
|
||||
<small>先看活跃链路是否闭环,再处理局部异常。</small>
|
||||
</div>
|
||||
<div className="dashboard-mini-card">
|
||||
<span>路由主线</span>
|
||||
<strong>平台模型 -> 供应商模型</strong>
|
||||
<small>优先保证模型绑定、价格和回调链路一致。</small>
|
||||
</div>
|
||||
<div className="dashboard-mini-card">
|
||||
<span>运行主线</span>
|
||||
<strong>本地 mock / 源码模式</strong>
|
||||
<small>适合先打磨前后台交互和管理体验。</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="stats-grid">
|
||||
{summaryCards.map((card) => (
|
||||
<article className="stat-card" key={card.label}>
|
||||
<h3>{card.label}</h3>
|
||||
<div className="value">{card.value}</div>
|
||||
<p className="muted">{card.copy}</p>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="two-col-grid">
|
||||
<article className="panel">
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<h3>今日关注</h3>
|
||||
<p className="muted">建议按下面的顺序巡检后台核心链路。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops-feed">
|
||||
<div className="ops-feed-item">
|
||||
<strong>1. 模型绑定和价格规则</strong>
|
||||
<span>保证前台展示模型和供应商实际路由保持一致,避免定价与能力错位。</span>
|
||||
</div>
|
||||
<div className="ops-feed-item">
|
||||
<strong>2. 任务状态与回调日志</strong>
|
||||
<span>遇到异常任务优先看回调载荷、最终状态和结果转存是否成功。</span>
|
||||
</div>
|
||||
<div className="ops-feed-item">
|
||||
<strong>3. 充值、兑换码和奖励关系</strong>
|
||||
<span>用户经济系统最好作为统一链路检查,而不是拆散在多个页面里看。</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="panel">
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<h3>运维视角</h3>
|
||||
<p className="muted">用三张小卡片快速定位应该先进哪个模块。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dashboard-stage-grid">
|
||||
<div className="dashboard-mini-card">
|
||||
<span>增长</span>
|
||||
<strong>用户 / 奖励 / 邀请</strong>
|
||||
<small>去用户管理、增长奖励、邀请关系。</small>
|
||||
</div>
|
||||
<div className="dashboard-mini-card">
|
||||
<span>供应商</span>
|
||||
<strong>账号 / 模型 / 绑定</strong>
|
||||
<small>去供应商账号、供应商模型、模型绑定。</small>
|
||||
</div>
|
||||
<div className="dashboard-mini-card">
|
||||
<span>资金与产出</span>
|
||||
<strong>订单 / 价格 / 任务</strong>
|
||||
<small>去充值订单、价格规则、视频任务。</small>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { startTransition, useState } from "react";
|
||||
import { ArrowRight, Blocks, Coins, Settings2, ShieldCheck } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { api, ApiError } from "@/lib/api";
|
||||
@@ -29,23 +31,58 @@ export default function AdminLoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-grid">
|
||||
<section style={{ padding: 48, display: "flex", flexDirection: "column", justifyContent: "space-between" }}>
|
||||
<div>
|
||||
<div className="login-grid auth-grid-admin">
|
||||
<section className="auth-hero auth-hero-admin">
|
||||
<div className="auth-copy">
|
||||
<div className="brand-kicker">Ops Console</div>
|
||||
<h1 style={{ fontSize: 64, lineHeight: 0.95, margin: "12px 0", fontFamily: "var(--font-display)" }}>
|
||||
管好模型、价格、奖励、订单和任务链路。
|
||||
</h1>
|
||||
<p className="muted" style={{ maxWidth: 580 }}>
|
||||
后台聚焦两件事:配置业务规则,以及处理异常链路。默认已创建管理员账号,适合直接联调整条 MVP。
|
||||
<h1 className="headline">把模型、价格、奖励、订单和任务链路收进一块控制台。</h1>
|
||||
<p className="subcopy">
|
||||
视觉上参考 `Linear/Vercel` 式精密控制台,信息密度更高,但仍保留足够留白,让配置、审计和异常排查更好读。
|
||||
</p>
|
||||
<div className="auth-chip-row">
|
||||
<span className="auth-chip">Provider routing</span>
|
||||
<span className="auth-chip">Pricing control</span>
|
||||
<span className="auth-chip">Audit trail</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="showcase-stack">
|
||||
<div className="showcase-card showcase-card-hero">
|
||||
<div className="toolbar">
|
||||
<span className="status-badge tone-success">Admin access</span>
|
||||
<span className="mini-note">Operational overview</span>
|
||||
</div>
|
||||
<h3>后台只做关键决策,不做噪音堆叠</h3>
|
||||
<p className="muted">
|
||||
当前后台围绕四个维度组织:用户经济系统、模型与供应商、任务状态以及公共配置。适合直接联调 MVP。
|
||||
</p>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
<section className="fullscreen-shell">
|
||||
<form className="auth-card" onSubmit={handleSubmit}>
|
||||
<div className="brand-kicker">AIVideo Admin</div>
|
||||
<h3>管理员登录</h3>
|
||||
<form className="auth-card auth-card-strong" onSubmit={handleSubmit}>
|
||||
<div className="auth-card-head">
|
||||
<div className="brand-kicker">AIVideo Admin</div>
|
||||
<h3>管理员登录</h3>
|
||||
<p className="muted">进入运营控制台,处理规则配置和异常链路。</p>
|
||||
</div>
|
||||
<div className="form-stack">
|
||||
<label className="field-label">
|
||||
用户名
|
||||
@@ -66,14 +103,23 @@ export default function AdminLoginPage() {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{error ? <div className="muted" style={{ color: "var(--danger)" }}>{error}</div> : null}
|
||||
{error ? <div className="inline-feedback is-error">{error}</div> : null}
|
||||
<button className="primary-button" type="submit">
|
||||
{loading ? "登录中..." : "进入后台"}
|
||||
{loading ? null : <ArrowRight size={16} />}
|
||||
</button>
|
||||
<div className="auth-action-row">
|
||||
<Link href="/login" className="ghost-button">
|
||||
返回用户端
|
||||
</Link>
|
||||
<div className="field-note">
|
||||
<ShieldCheck size={14} />
|
||||
默认管理员账号已由后端初始化脚本自动创建。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
17
frontend-web/src/app/fonts.ts
Normal file
17
frontend-web/src/app/fonts.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IBM_Plex_Mono, Manrope, Syne } from "next/font/google";
|
||||
|
||||
export const displayFont = Syne({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-display",
|
||||
});
|
||||
|
||||
export const bodyFont = Manrope({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-body",
|
||||
});
|
||||
|
||||
export const monoFont = IBM_Plex_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-mono",
|
||||
weight: ["400", "500", "600"],
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,13 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import { bodyFont, displayFont, monoFont } from "@/app/fonts";
|
||||
import { Providers } from "@/components/providers";
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AIVideo",
|
||||
description: "AI 视频生成平台用户前台",
|
||||
description: "AIVideo AI 视频创作与运营控制台",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -15,7 +16,10 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<html
|
||||
lang="zh-CN"
|
||||
className={`${displayFont.variable} ${bodyFont.variable} ${monoFont.variable}`}
|
||||
>
|
||||
<body className="app-fonts">
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { startTransition, useState } from "react";
|
||||
import { ArrowRight, Clapperboard, Coins, FolderKanban, Sparkles } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -28,27 +29,57 @@ export default function LoginPage() {
|
||||
|
||||
return (
|
||||
<div className="auth-grid">
|
||||
<section className="auth-hero">
|
||||
<div>
|
||||
<section className="auth-hero auth-hero-studio">
|
||||
<div className="auth-copy">
|
||||
<div className="eyebrow">Creative Workbench</div>
|
||||
<h1 className="headline">从提示词到成片,完整跑通你的 AI 视频生产线。</h1>
|
||||
<h1 className="headline">从一句提示词到成片交付,把 AI 视频生产线收进一块工作台。</h1>
|
||||
<p className="subcopy">
|
||||
支持充值积分、兑换密钥、素材上传、供应商模型路由和任务轮询。当前默认已接入本地 mock
|
||||
OpenAI / Seedance 流程,开箱即可联调。
|
||||
参考 `RunwayML` 式的电影感操作界面,把模型切换、素材引用、积分消耗和任务轮询收敛为一个连续动作。
|
||||
</p>
|
||||
<div className="auth-chip-row">
|
||||
<span className="auth-chip">Prompt to video</span>
|
||||
<span className="auth-chip">Asset-aware workflow</span>
|
||||
<span className="auth-chip">Realtime task polling</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="panel">
|
||||
<h3>默认体验方式</h3>
|
||||
<p className="muted">
|
||||
如果还没有账号,直接去注册。注册成功后会自动发放注册奖励积分。
|
||||
</p>
|
||||
<div className="showcase-stack">
|
||||
<div className="showcase-card showcase-card-hero">
|
||||
<div className="toolbar">
|
||||
<span className="status-badge tone-success">Studio ready</span>
|
||||
<span className="mini-note">Source mode</span>
|
||||
</div>
|
||||
<h3>一次登录,接管创作链路</h3>
|
||||
<p className="muted">
|
||||
默认即可走本地 mock 供应商、SQLite 和本地存储,适合先把完整交互、任务状态和后台规则跑通。
|
||||
</p>
|
||||
</div>
|
||||
<div className="showcase-card-grid">
|
||||
<article className="showcase-card">
|
||||
<Clapperboard size={18} />
|
||||
<strong>工作台直达成片</strong>
|
||||
<span>模型、时长、比例与素材引用在同一视图完成。</span>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
<section className="fullscreen-shell">
|
||||
<form className="auth-card" onSubmit={handleSubmit}>
|
||||
<div className="eyebrow">Welcome Back</div>
|
||||
<h2 style={{ marginTop: 8 }}>登录 AIVideo</h2>
|
||||
<form className="auth-card auth-card-strong" onSubmit={handleSubmit}>
|
||||
<div className="auth-card-head">
|
||||
<div className="eyebrow">Welcome Back</div>
|
||||
<h2>登录 AIVideo</h2>
|
||||
<p className="muted">继续你的创作队列、素材库和积分操作。</p>
|
||||
</div>
|
||||
<div className="form-stack">
|
||||
<label className="field-label">
|
||||
账号
|
||||
@@ -69,17 +100,23 @@ export default function LoginPage() {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{error ? <div className="muted" style={{ color: "var(--danger)" }}>{error}</div> : null}
|
||||
{error ? <div className="inline-feedback is-error">{error}</div> : null}
|
||||
<button className="primary-button" disabled={loading} type="submit">
|
||||
{loading ? "登录中..." : "登录并进入工作台"}
|
||||
{loading ? null : <ArrowRight size={16} />}
|
||||
</button>
|
||||
<Link href="/register" className="ghost-button">
|
||||
去注册
|
||||
</Link>
|
||||
<div className="auth-action-row">
|
||||
<Link href="/register" className="ghost-button">
|
||||
创建账号
|
||||
</Link>
|
||||
<div className="field-note">
|
||||
<Sparkles size={14} />
|
||||
新用户注册后可按后台规则领取初始积分。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,25 +16,25 @@ import {
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
const navigation = [
|
||||
{ href: "/admin/dashboard", label: "仪表盘", icon: ChartColumnBig },
|
||||
{ href: "/admin/users", label: "用户管理", icon: Users },
|
||||
{ href: "/admin/recharge-orders", label: "充值订单", icon: Coins },
|
||||
{ href: "/admin/redeem-codes", label: "兑换密钥", icon: KeySquare },
|
||||
{ href: "/admin/growth-rules", label: "增长奖励", icon: Link2 },
|
||||
{ href: "/admin/invite-relations", label: "邀请关系", icon: Link2 },
|
||||
{ href: "/admin/provider-accounts", label: "供应商账号", icon: Workflow },
|
||||
{ href: "/admin/provider-models", label: "供应商模型", icon: Blocks },
|
||||
{ href: "/admin/video-models", label: "平台模型", icon: Package2 },
|
||||
{ href: "/admin/video-model-bindings", label: "模型绑定", icon: Workflow },
|
||||
{ href: "/admin/pricing-rules", label: "价格规则", icon: Coins },
|
||||
{ href: "/admin/video-tasks", label: "视频任务", icon: Workflow },
|
||||
{ href: "/admin/callback-logs", label: "回调日志", icon: ChartColumnBig },
|
||||
{ href: "/admin/system-config", label: "系统配置", icon: Settings2 },
|
||||
{ href: "/admin/dashboard", label: "仪表盘", description: "查看系统总览、支付与任务健康度", icon: ChartColumnBig },
|
||||
{ href: "/admin/users", label: "用户管理", description: "维护用户状态、余额与基本资料", icon: Users },
|
||||
{ href: "/admin/recharge-orders", label: "充值订单", description: "跟踪充值流转与支付状态", icon: Coins },
|
||||
{ href: "/admin/redeem-codes", label: "兑换密钥", description: "批量生成、停用与审计兑换码", icon: KeySquare },
|
||||
{ href: "/admin/growth-rules", label: "增长奖励", description: "配置注册奖励与邀请返利规则", icon: Link2 },
|
||||
{ href: "/admin/invite-relations", label: "邀请关系", description: "查看邀请码链路与归因结果", icon: Link2 },
|
||||
{ href: "/admin/provider-accounts", label: "供应商账号", description: "管理各供应商 baseUrl 与鉴权信息", icon: Workflow },
|
||||
{ href: "/admin/provider-models", label: "供应商模型", description: "维护供应商模型能力与协议", icon: Blocks },
|
||||
{ href: "/admin/video-models", label: "平台模型", description: "定义前台可见的统一模型层", icon: Package2 },
|
||||
{ href: "/admin/video-model-bindings", label: "模型绑定", description: "决定平台模型到供应商模型的路由", icon: Workflow },
|
||||
{ href: "/admin/pricing-rules", label: "价格规则", description: "按模型与版本维护积分定价", icon: Coins },
|
||||
{ href: "/admin/video-tasks", label: "视频任务", description: "处理异常任务与追踪回调结果", icon: Workflow },
|
||||
{ href: "/admin/callback-logs", label: "回调日志", description: "查看供应商回调负载与重放线索", icon: ChartColumnBig },
|
||||
{ href: "/admin/system-config", label: "系统配置", description: "维护站点公告、策略与公开配置", icon: Settings2 },
|
||||
];
|
||||
|
||||
export function AdminShell({ children }: { children: React.ReactNode }) {
|
||||
@@ -51,6 +51,14 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, [meQuery.error, router]);
|
||||
|
||||
const current = useMemo(
|
||||
() =>
|
||||
[...navigation]
|
||||
.sort((left, right) => right.href.length - left.href.length)
|
||||
.find((item) => pathname.startsWith(item.href)) ?? navigation[0],
|
||||
[pathname],
|
||||
);
|
||||
|
||||
if (meQuery.isLoading || !meQuery.data) {
|
||||
return (
|
||||
<div className="fullscreen-shell">
|
||||
@@ -60,16 +68,43 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shell-grid">
|
||||
<div className="shell-grid shell-grid-admin">
|
||||
<aside className="shell-sidebar">
|
||||
<div className="brand-block">
|
||||
<div className="brand-block brand-block-admin">
|
||||
<div className="brand-visual" aria-hidden="true">
|
||||
<span className="brand-orb brand-orb-primary" />
|
||||
<span className="brand-orb brand-orb-secondary" />
|
||||
</div>
|
||||
<span className="brand-kicker">AIVIDEO ADMIN</span>
|
||||
<h1>控制后台</h1>
|
||||
<p>围绕模型、价格、奖励、订单和任务链路进行统一配置与审计。</p>
|
||||
<p>围绕模型、价格、奖励、订单和任务链路做统一配置、审计和异常处置。</p>
|
||||
<div className="brand-pill-row">
|
||||
<span className="brand-pill">Ops control</span>
|
||||
<span className="brand-pill">Pricing</span>
|
||||
<span className="brand-pill">Provider routing</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-note">
|
||||
<div className="sidebar-note-label">Operations Grid</div>
|
||||
<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) => {
|
||||
{navigation.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
@@ -80,14 +115,23 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
|
||||
})}
|
||||
>
|
||||
<Icon size={18} />
|
||||
<span>{item.label}</span>
|
||||
<div>
|
||||
<span>{item.label}</span>
|
||||
<small>{item.description}</small>
|
||||
</div>
|
||||
<div className="nav-index">{String(index + 1).padStart(2, "0")}</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="profile-card">
|
||||
<div>
|
||||
<div className="profile-card-main">
|
||||
<div className="profile-avatar">
|
||||
{String((meQuery.data as { nickname: string }).nickname ?? "AD")
|
||||
.slice(0, 2)
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
<div className="profile-name">
|
||||
{(meQuery.data as { nickname: string }).nickname}
|
||||
</div>
|
||||
@@ -95,6 +139,16 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
|
||||
{(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>
|
||||
<button
|
||||
className="ghost-button"
|
||||
onClick={async () => {
|
||||
@@ -109,9 +163,19 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
|
||||
</aside>
|
||||
|
||||
<main className="shell-main">
|
||||
<header className="shell-header">
|
||||
<div className="shell-header-content">
|
||||
<div className="header-kicker">Operations Console</div>
|
||||
<h2>{current.label}</h2>
|
||||
<p className="shell-header-copy">{current.description}</p>
|
||||
</div>
|
||||
<div className="shell-header-meta">
|
||||
<div className="header-chip header-chip-success">管理员会话已建立</div>
|
||||
<div className="header-chip">统一配置 / 审计 / 处置</div>
|
||||
</div>
|
||||
</header>
|
||||
<section className="shell-content">{children}</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { startTransition, useState } from "react";
|
||||
import { ArrowRight, Coins, Gift, Link2, Sparkles } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -36,24 +37,57 @@ export function RegisterForm({ inviteCode = "" }: { inviteCode?: string }) {
|
||||
|
||||
return (
|
||||
<div className="auth-grid">
|
||||
<section className="auth-hero">
|
||||
<div>
|
||||
<section className="auth-hero auth-hero-studio">
|
||||
<div className="auth-copy">
|
||||
<div className="eyebrow">New Account</div>
|
||||
<h1 className="headline">用一个账号串起充值、任务、邀请奖励和素材管理。</h1>
|
||||
<h1 className="headline">用一个账号,串起创作、积分、邀请奖励和素材管理。</h1>
|
||||
<p className="subcopy">
|
||||
注册后会读取后台增长奖励规则,若已开启则自动发放注册积分;如果通过邀请码注册,也会建立邀请关系。
|
||||
注册后会读取后台增长规则发放奖励;如果带邀请码进入,则会同步建立邀请关系,后续消费可自动回传奖励。
|
||||
</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 className="panel">
|
||||
<h3>邀请奖励说明</h3>
|
||||
<p className="muted">被邀请用户首次有效消费成功后,邀请人可自动拿到奖励积分。</p>
|
||||
<div className="showcase-stack">
|
||||
<div className="showcase-card showcase-card-hero">
|
||||
<div className="toolbar">
|
||||
<span className="status-badge tone-soft">Growth loop</span>
|
||||
<span className="mini-note">Reward rules active</span>
|
||||
</div>
|
||||
<h3>把增长规则自然接到用户首登体验里</h3>
|
||||
<p className="muted">
|
||||
注册成功后直接进入工作台,不需要在多个页面之间跳转,就能继续上传素材、创建任务和消费积分。
|
||||
</p>
|
||||
</div>
|
||||
<div className="showcase-card-grid">
|
||||
<article className="showcase-card">
|
||||
<Gift size={18} />
|
||||
<strong>注册奖励自动发放</strong>
|
||||
<span>遵循后台增长配置,不在前台写死业务规则。</span>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
<section className="fullscreen-shell">
|
||||
<form className="auth-card" onSubmit={handleSubmit}>
|
||||
<div className="eyebrow">Create Account</div>
|
||||
<h2 style={{ marginTop: 8 }}>注册用户</h2>
|
||||
<form className="auth-card auth-card-strong" onSubmit={handleSubmit}>
|
||||
<div className="auth-card-head">
|
||||
<div className="eyebrow">Create Account</div>
|
||||
<h2>注册用户</h2>
|
||||
<p className="muted">创建账号后会直接进入创作端工作台。</p>
|
||||
</div>
|
||||
<div className="form-stack">
|
||||
<label className="field-label">
|
||||
邮箱账号
|
||||
@@ -84,17 +118,23 @@ export function RegisterForm({ inviteCode = "" }: { inviteCode?: string }) {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{error ? <div className="muted" style={{ color: "var(--danger)" }}>{error}</div> : null}
|
||||
{error ? <div className="inline-feedback is-error">{error}</div> : null}
|
||||
<button className="primary-button" disabled={loading} type="submit">
|
||||
{loading ? "注册中..." : "注册并领取初始积分"}
|
||||
{loading ? null : <ArrowRight size={16} />}
|
||||
</button>
|
||||
<Link href="/login" className="ghost-button">
|
||||
返回登录
|
||||
</Link>
|
||||
<div className="auth-action-row">
|
||||
<Link href="/login" className="ghost-button">
|
||||
返回登录
|
||||
</Link>
|
||||
<div className="field-note">
|
||||
<Sparkles size={14} />
|
||||
若通过邀请链接进入,邀请码会自动带入当前表单。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,20 +105,47 @@ export function SiteShell({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shell-grid">
|
||||
<div className="shell-grid shell-grid-studio">
|
||||
<aside className="shell-sidebar">
|
||||
<div className="brand-block">
|
||||
<div className="brand-block brand-block-studio">
|
||||
<div className="brand-visual" aria-hidden="true">
|
||||
<span className="brand-orb brand-orb-primary" />
|
||||
<span className="brand-orb brand-orb-secondary" />
|
||||
</div>
|
||||
<span className="brand-kicker">AI VIDEO STUDIO</span>
|
||||
<h1>AIVideo</h1>
|
||||
<p>把模型路由、素材管理和任务轮询收进一个暗色工作台,专门给创作流程用。</p>
|
||||
<p>把模型路由、素材管理和任务轮询收进一个电影感工作台,让创作动作像调度镜头一样顺滑。</p>
|
||||
<div className="brand-pill-row">
|
||||
<span className="brand-pill">Source mode</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 className="sidebar-note">
|
||||
<div className="sidebar-note-label">Pipeline Focus</div>
|
||||
<div className="sidebar-metrics">
|
||||
<div className="sidebar-metric">
|
||||
<strong>Prompt</strong>
|
||||
<span>镜头、动作、光感</span>
|
||||
</div>
|
||||
<div className="sidebar-metric">
|
||||
<strong>Assets</strong>
|
||||
<span>图片、视频、音频参考</span>
|
||||
</div>
|
||||
<div className="sidebar-metric">
|
||||
<strong>Tasks</strong>
|
||||
<span>轮询、冻结、结果落盘</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="sidebar-nav">
|
||||
{navigation.map((item) => {
|
||||
{navigation.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
@@ -133,6 +160,7 @@ export function SiteShell({ children }: { children: React.ReactNode }) {
|
||||
<span>{item.label}</span>
|
||||
<small>{item.description}</small>
|
||||
</div>
|
||||
<div className="nav-index">{String(index + 1).padStart(2, "0")}</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
@@ -148,6 +176,16 @@ export function SiteShell({ children }: { children: React.ReactNode }) {
|
||||
<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>
|
||||
<button
|
||||
className="ghost-button"
|
||||
onClick={async () => {
|
||||
@@ -163,7 +201,7 @@ export function SiteShell({ children }: { children: React.ReactNode }) {
|
||||
|
||||
<main className="shell-main">
|
||||
<header className="shell-header">
|
||||
<div>
|
||||
<div className="shell-header-content">
|
||||
<div className="header-kicker">Creative Ops Console</div>
|
||||
<h2>{title.label}</h2>
|
||||
<p className="shell-header-copy">{title.description}</p>
|
||||
@@ -173,7 +211,8 @@ export function SiteShell({ children }: { children: React.ReactNode }) {
|
||||
<Activity size={14} />
|
||||
会话正常
|
||||
</div>
|
||||
<div className="header-chip">{data.publicId}</div>
|
||||
<div className="header-chip">{data.publicId || "UNAVAILABLE"}</div>
|
||||
<div className="header-chip">本地素材 / 本地存储 / mock 供应商</div>
|
||||
</div>
|
||||
</header>
|
||||
<section className="shell-content">{children}</section>
|
||||
|
||||
Reference in New Issue
Block a user