交互原型大版本
This commit is contained in:
251
art-agent/frontend/src/app/styles/page.tsx
Normal file
251
art-agent/frontend/src/app/styles/page.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { TopNav } from "@/components/layout/top-nav";
|
||||
import { AmbientParticles } from "@/components/ui/ambient-particles";
|
||||
import { useApp } from "@/lib/app-context";
|
||||
import { staggerContainer, staggerItem } from "@/components/ui/motion-presets";
|
||||
import type { StylePack } from "@/lib/types";
|
||||
|
||||
type SourceFilter = "all" | "preset" | "custom" | "community";
|
||||
|
||||
const ALL_KEYWORDS = ["写实", "二次元", "像素", "水墨", "赛博", "奇幻", "写意", "科幻", "复古"];
|
||||
|
||||
export default function StylesPage() {
|
||||
const { stylePacks } = useApp();
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
const [source, setSource] = useState<SourceFilter>("all");
|
||||
const [keyword, setKeyword] = useState<string | null>(null);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
let list = [...stylePacks];
|
||||
if (source !== "all") list = list.filter((s) => s.source === source);
|
||||
if (keyword) list = list.filter((s) => s.keywords.includes(keyword));
|
||||
if (query.trim()) {
|
||||
const q = query.trim().toLowerCase();
|
||||
list = list.filter(
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(q) ||
|
||||
s.description.toLowerCase().includes(q) ||
|
||||
s.keywords.some((k) => k.toLowerCase().includes(q))
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}, [stylePacks, source, keyword, query]);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col relative z-[1]">
|
||||
<AmbientParticles count={16} />
|
||||
<TopNav />
|
||||
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<div className="max-w-7xl mx-auto px-4 md:px-8 py-6 md:py-8">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-start justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<h1 className="typo-h1">风格库</h1>
|
||||
<span className="phase-chip">Phase 1</span>
|
||||
</div>
|
||||
<p className="typo-caption">
|
||||
项目级风格集。每个风格包含关键词、参考集、Prompt 模板和(可选的)LoRA 绑定。
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/styles/new"
|
||||
className="flex items-center gap-1.5 px-3.5 py-2 text-sm rounded-xl font-medium
|
||||
bg-[var(--accent)] text-[var(--bg-primary)]
|
||||
hover:bg-[var(--accent-hover)]
|
||||
hover:shadow-[0_0_12px_rgba(46,139,122,0.25)]
|
||||
transition-all cursor-pointer btn-hover-lift flex-shrink-0"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
新建风格
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 工具栏 */}
|
||||
<div className="flex flex-wrap items-center gap-3 mb-5">
|
||||
<div className="flex items-center gap-1.5 flex-1 min-w-[200px] max-w-[360px]
|
||||
px-3 py-1.5 rounded-xl bg-[var(--bg-tertiary)] border border-[var(--border)]
|
||||
focus-within:border-[var(--accent)]/40 transition-colors">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--text-secondary)" strokeWidth="2">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="M21 21l-4.35-4.35" />
|
||||
</svg>
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="搜索风格名 / 描述 / 关键词..."
|
||||
className="flex-1 bg-transparent border-none text-sm
|
||||
text-[var(--text-primary)] placeholder:text-[var(--text-secondary)]
|
||||
focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 来源筛选 */}
|
||||
<div className="flex items-center gap-0.5 bg-[var(--bg-tertiary)] rounded-lg p-0.5 border border-[var(--border)]">
|
||||
{(["all", "preset", "custom", "community"] as const).map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setSource(s)}
|
||||
className={`px-2.5 py-1 text-xs rounded-md cursor-pointer transition-all ${
|
||||
source === s
|
||||
? "bg-[var(--accent)]/10 text-[var(--accent)] font-medium"
|
||||
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||
}`}
|
||||
>
|
||||
{s === "all" ? "全部" : s === "preset" ? "预设" : s === "custom" ? "自建" : "社区"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分类筛选(关键词) */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 mb-6">
|
||||
<span className="typo-micro mr-1" style={{ textTransform: "none" }}>分类:</span>
|
||||
<button
|
||||
onClick={() => setKeyword(null)}
|
||||
className={`px-2.5 py-1 text-[11px] rounded-lg cursor-pointer transition-all ${
|
||||
!keyword
|
||||
? "bg-[var(--accent)]/10 text-[var(--accent)] border border-[var(--accent)]/30"
|
||||
: "text-[var(--text-secondary)] border border-transparent hover:border-[var(--border)]"
|
||||
}`}
|
||||
>
|
||||
全部
|
||||
</button>
|
||||
{ALL_KEYWORDS.map((k) => {
|
||||
const active = keyword === k;
|
||||
return (
|
||||
<button
|
||||
key={k}
|
||||
onClick={() => setKeyword(active ? null : k)}
|
||||
className={`px-2.5 py-1 text-[11px] rounded-lg cursor-pointer transition-all ${
|
||||
active
|
||||
? "bg-[var(--accent)]/10 text-[var(--accent)] border border-[var(--accent)]/30"
|
||||
: "text-[var(--text-secondary)] border border-transparent hover:border-[var(--border)]"
|
||||
}`}
|
||||
>
|
||||
{k}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 卡片网格 */}
|
||||
{filtered.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="typo-display text-lg mb-2">没有匹配的风格</p>
|
||||
<p className="typo-caption">试试调整筛选条件或点击“新建风格”</p>
|
||||
</div>
|
||||
) : (
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"
|
||||
>
|
||||
{filtered.map((pack) => (
|
||||
<StyleCard key={pack.id} pack={pack} />
|
||||
))}
|
||||
{/* 新建卡片 */}
|
||||
<motion.div variants={staggerItem}>
|
||||
<Link
|
||||
href="/styles/new"
|
||||
className="aspect-[3/4] rounded-xl placeholder-card
|
||||
flex flex-col items-center justify-center gap-2
|
||||
text-[var(--text-secondary)] hover:text-[var(--accent)]
|
||||
hover:border-[var(--accent)]/50 cursor-pointer
|
||||
transition-all btn-hover-lift"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
<span className="text-xs">新建风格</span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StyleCard({ pack }: { pack: StylePack }) {
|
||||
const sourceLabel =
|
||||
pack.source === "preset" ? "预设" : pack.source === "custom" ? "自建" : "社区";
|
||||
|
||||
return (
|
||||
<motion.div variants={staggerItem}>
|
||||
<Link
|
||||
href={`/styles/${pack.id}`}
|
||||
className="block rounded-xl overflow-hidden neon-border
|
||||
bg-[var(--bg-card)] backdrop-blur-sm cursor-pointer
|
||||
transition-all"
|
||||
>
|
||||
{/* 代表图(占位) */}
|
||||
<div className="aspect-[4/3] relative overflow-hidden
|
||||
bg-gradient-to-br from-[var(--accent)]/15 via-[var(--accent-secondary)]/10 to-transparent">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold text-[var(--accent)] tracking-tight opacity-60">
|
||||
{pack.name.slice(0, 2)}
|
||||
</div>
|
||||
<div className="text-[9px] text-[var(--text-secondary)] mt-1">
|
||||
代表图占位({pack.refImages.length} 张参考)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 来源标签 */}
|
||||
<span
|
||||
className="absolute top-2 right-2 text-[9px] px-1.5 py-0.5 rounded-md font-medium"
|
||||
style={{
|
||||
background:
|
||||
pack.source === "preset"
|
||||
? "rgba(46, 139, 122, 0.15)"
|
||||
: pack.source === "custom"
|
||||
? "rgba(58, 127, 184, 0.15)"
|
||||
: "rgba(184, 147, 90, 0.15)",
|
||||
color:
|
||||
pack.source === "preset"
|
||||
? "var(--accent)"
|
||||
: pack.source === "custom"
|
||||
? "var(--accent-secondary)"
|
||||
: "var(--gold)",
|
||||
}}
|
||||
>
|
||||
{sourceLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3">
|
||||
<div className="text-sm font-medium text-[var(--text-primary)] truncate mb-1">
|
||||
{pack.name}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{pack.keywords.slice(0, 3).map((k) => (
|
||||
<span
|
||||
key={k}
|
||||
className="text-[9px] px-1.5 py-0.5 rounded-md
|
||||
bg-[var(--accent)]/8 text-[var(--accent)] font-medium"
|
||||
>
|
||||
{k}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[10px] text-[var(--text-secondary)]">
|
||||
<span>使用 {pack.usageCount} 次</span>
|
||||
{pack.loraId && <span className="phase-chip-accent phase-chip">LoRA</span>}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user