小优化,产品愿景,视觉规范,交互和导航大版本
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { TopNav } from "@/components/layout/top-nav";
|
||||
import { AmbientParticles } from "@/components/ui/ambient-particles";
|
||||
import { useFireflyBurst, FireflyBurst } from "@/components/ui/firefly-burst";
|
||||
import { useApp } from "@/lib/app-context";
|
||||
import { getImageUrl } from "@/lib/api";
|
||||
import { staggerContainer, staggerItem, slideUp } from "@/components/ui/motion-presets";
|
||||
import type { ImageAsset } from "@/lib/types";
|
||||
|
||||
type ViewMode = "grid" | "list";
|
||||
@@ -110,12 +114,13 @@ export default function GalleryPage() {
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col relative z-[1]">
|
||||
<AmbientParticles count={filtered.length === 0 ? 25 : 15} />
|
||||
<TopNav />
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<main className="flex-1 flex flex-col min-w-0">
|
||||
{/* 工具栏 */}
|
||||
<div className="flex-shrink-0 border-b border-[var(--border)] bg-[var(--bg-secondary)]/80
|
||||
<div className="flex-shrink-0 border-b border-[var(--border)] surface-1
|
||||
backdrop-blur-xl px-4 md:px-5 py-3
|
||||
flex items-center gap-2 md:gap-3 flex-wrap">
|
||||
{/* 区块标题 */}
|
||||
@@ -125,7 +130,7 @@ export default function GalleryPage() {
|
||||
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||
<polyline points="21 15 16 10 5 21" />
|
||||
</svg>
|
||||
<span className="text-sm font-semibold text-[var(--text-primary)]">资源库</span>
|
||||
<span className="typo-strong text-sm">资源库</span>
|
||||
</div>
|
||||
|
||||
{/* 搜索 */}
|
||||
@@ -272,19 +277,28 @@ export default function GalleryPage() {
|
||||
<div className="flex-1 overflow-y-auto p-4 md:p-5">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl
|
||||
bg-[var(--accent)]/10 border border-[var(--accent)]/20">
|
||||
<motion.div
|
||||
className="text-center space-y-4"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<motion.div variants={staggerItem} className="inline-flex items-center justify-center w-14 h-14 rounded-2xl
|
||||
bg-[var(--accent)]/10 border border-[var(--accent)]/20
|
||||
shadow-[0_0_20px_rgba(46,139,122,0.1)]">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="1.5" opacity="0.6">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||
<polyline points="21 15 16 10 5 21" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
</motion.div>
|
||||
<motion.p variants={staggerItem} className="typo-display text-lg">
|
||||
{assets.length === 0 ? "还没有生成过图片" : "没有匹配的资源"}
|
||||
</p>
|
||||
</div>
|
||||
</motion.p>
|
||||
<motion.p variants={staggerItem} className="typo-caption">
|
||||
{assets.length === 0 ? "在对话中生成的图片会自动出现在这里" : "试试调整筛选条件"}
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : viewMode === "grid" ? (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3 md:gap-4">
|
||||
@@ -294,7 +308,7 @@ export default function GalleryPage() {
|
||||
className={`group relative rounded-xl overflow-hidden neon-border cursor-pointer
|
||||
bg-[var(--bg-card)] backdrop-blur-sm transition-all ${
|
||||
selectedIds.has(asset.id)
|
||||
? "!border-[var(--accent)] ring-1 ring-[var(--accent)]/30 shadow-[0_0_15px_rgba(77,184,164,0.08)]"
|
||||
? "!border-[var(--accent)] ring-1 ring-[var(--accent)]/30 shadow-[0_0_15px_rgba(46,139,122,0.08)]"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setDetailAsset(asset)}
|
||||
@@ -310,7 +324,7 @@ export default function GalleryPage() {
|
||||
<div
|
||||
className={`w-5 h-5 rounded-md border-2 flex items-center justify-center transition-all ${
|
||||
selectedIds.has(asset.id)
|
||||
? "bg-[var(--accent)] border-[var(--accent)] shadow-[0_0_8px_rgba(77,184,164,0.25)]"
|
||||
? "bg-[var(--accent)] border-[var(--accent)] shadow-[0_0_8px_rgba(46,139,122,0.25)]"
|
||||
: "border-white/40 bg-black/30 backdrop-blur-sm opacity-100 md:opacity-0 md:group-hover:opacity-100"
|
||||
}`}
|
||||
>
|
||||
@@ -527,15 +541,17 @@ function GalleryDetailModal({
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="glass-panel rounded-2xl max-w-3xl w-full mx-2 md:mx-4
|
||||
max-h-[90vh] md:max-h-[85vh] overflow-hidden flex flex-col
|
||||
shadow-xl shadow-black/40"
|
||||
<motion.div
|
||||
variants={slideUp}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="surface-3 rounded-2xl max-w-3xl w-full mx-2 md:mx-4
|
||||
max-h-[90vh] md:max-h-[85vh] overflow-hidden flex flex-col"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between px-5 py-3.5 border-b border-[var(--border)]">
|
||||
<span className="text-sm font-semibold text-[var(--text-primary)]">资源详情</span>
|
||||
<span className="typo-h2">资源详情</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-[var(--text-secondary)] hover:text-[var(--accent)] cursor-pointer
|
||||
@@ -565,7 +581,7 @@ function GalleryDetailModal({
|
||||
onClick={onDownload}
|
||||
className="flex items-center gap-1.5 px-3.5 py-2 text-xs rounded-xl font-medium
|
||||
bg-[var(--accent)] text-[var(--bg-primary)]
|
||||
hover:bg-[var(--accent-hover)] hover:shadow-[0_0_12px_rgba(77,184,164,0.25)]
|
||||
hover:bg-[var(--accent-hover)] hover:shadow-[0_0_12px_rgba(46,139,122,0.25)]
|
||||
cursor-pointer transition-all"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
@@ -602,7 +618,7 @@ function GalleryDetailModal({
|
||||
{asset.prompt && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">Prompt</span>
|
||||
<span className="typo-strong text-xs">Prompt</span>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(asset.prompt)}
|
||||
className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--accent)]
|
||||
@@ -621,7 +637,7 @@ function GalleryDetailModal({
|
||||
{/* 标签 */}
|
||||
{asset.tags.length > 0 && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)] block mb-1.5">标签</span>
|
||||
<span className="typo-strong text-xs block mb-1.5">标签</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{asset.tags.map((tid) => {
|
||||
const tag = tagMap.get(tid);
|
||||
@@ -648,7 +664,7 @@ function GalleryDetailModal({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* ===== 青绿山水色彩体系 ===== */
|
||||
/* ===== 青绿山水色彩体系(白底·绢本) ===== */
|
||||
:root {
|
||||
--bg-primary: #0C1210;
|
||||
--bg-secondary: #131E1A;
|
||||
--bg-tertiary: #1C2A24;
|
||||
--bg-card: rgba(16, 30, 24, 0.65);
|
||||
--text-primary: #E0E8E2;
|
||||
--text-secondary: #7A9485;
|
||||
--accent: #4DB8A4;
|
||||
--accent-hover: #6CD4BE;
|
||||
--accent-secondary: #3A8FB7;
|
||||
--bg-primary: #FAFAF7;
|
||||
--bg-secondary: #F2F1EC;
|
||||
--bg-tertiary: #E8E6DF;
|
||||
--bg-card: rgba(255, 255, 255, 0.8);
|
||||
--text-primary: #1A1A1A;
|
||||
--text-secondary: #6B7B8A;
|
||||
--accent: #2E8B7A;
|
||||
--accent-hover: #3AA38E;
|
||||
--accent-secondary: #3A7FB8;
|
||||
--hot: #C4654A;
|
||||
--border: rgba(77, 184, 164, 0.12);
|
||||
--border-glow: rgba(77, 184, 164, 0.25);
|
||||
--mist: rgba(77, 184, 164, 0.06);
|
||||
--border: rgba(46, 139, 122, 0.15);
|
||||
--border-glow: rgba(46, 139, 122, 0.30);
|
||||
--mist: rgba(46, 139, 122, 0.04);
|
||||
--gold: #B8935A;
|
||||
}
|
||||
|
||||
@@ -22,8 +22,126 @@ body {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.01em;
|
||||
overscroll-behavior: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
/* 绢本底纹:极淡暖白,像宣纸的自然质感 */
|
||||
background-image:
|
||||
radial-gradient(ellipse at 20% 80%, rgba(46, 139, 122, 0.03) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 20%, rgba(58, 127, 184, 0.02) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
/* ===== 排版层级体系(7 级) ===== */
|
||||
|
||||
.typo-display {
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.2;
|
||||
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-secondary) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.typo-h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.3;
|
||||
color: var(--text-primary);
|
||||
position: relative;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.typo-h1::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, var(--accent) 0%, transparent 100%);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.typo-h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.4;
|
||||
color: var(--text-primary);
|
||||
padding-left: 12px;
|
||||
position: relative;
|
||||
}
|
||||
.typo-h2::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
width: 3px;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.typo-strong {
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.typo-caption {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.typo-micro {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ===== 表面层级系统(Surface Elevation) ===== */
|
||||
|
||||
.surface-1 {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.surface-2 {
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
backdrop-filter: blur(20px) saturate(1.1);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.1);
|
||||
border: 1px solid rgba(46, 139, 122, 0.12);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06),
|
||||
0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.surface-3 {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(24px) saturate(1.15);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(1.15);
|
||||
border: 1px solid rgba(46, 139, 122, 0.18);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08),
|
||||
0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* ===== 网格底纹(绢本纹理潜意识层) ===== */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(circle, var(--accent) 0.5px, transparent 0.5px);
|
||||
background-size: 24px 24px;
|
||||
opacity: 0.018;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ===== 山雾背景 — 远景层 ===== */
|
||||
@@ -41,60 +159,50 @@ body {
|
||||
100% { transform: translate(0, 0) scale(1); }
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
.fog-layer-far {
|
||||
position: fixed;
|
||||
inset: -10%;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 15% 75%, rgba(77, 184, 164, 0.07) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 60% 40% at 75% 25%, rgba(58, 143, 183, 0.05) 0%, transparent 55%),
|
||||
radial-gradient(ellipse 90% 60% at 50% 50%, rgba(77, 184, 164, 0.03) 0%, transparent 70%);
|
||||
radial-gradient(ellipse 80% 50% at 15% 75%, rgba(46, 139, 122, 0.04) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 60% 40% at 75% 25%, rgba(58, 127, 184, 0.03) 0%, transparent 55%),
|
||||
radial-gradient(ellipse 90% 60% at 50% 50%, rgba(46, 139, 122, 0.02) 0%, transparent 70%);
|
||||
animation: fogDriftFar 60s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
body::after {
|
||||
content: '';
|
||||
.fog-layer-near {
|
||||
position: fixed;
|
||||
inset: -15%;
|
||||
background:
|
||||
radial-gradient(ellipse 70% 45% at 80% 70%, rgba(77, 184, 164, 0.06) 0%, transparent 55%),
|
||||
radial-gradient(ellipse 50% 60% at 25% 30%, rgba(58, 143, 183, 0.04) 0%, transparent 50%);
|
||||
radial-gradient(ellipse 70% 45% at 80% 70%, rgba(46, 139, 122, 0.03) 0%, transparent 55%),
|
||||
radial-gradient(ellipse 50% 60% at 25% 30%, rgba(58, 127, 184, 0.025) 0%, transparent 50%);
|
||||
animation: fogDriftNear 45s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ===== 山雾玻璃面板 ===== */
|
||||
/* ===== 山雾玻璃面板(兼容旧用法) ===== */
|
||||
.glass-panel {
|
||||
background:
|
||||
linear-gradient(165deg, rgba(16, 30, 24, 0.15) 0%, transparent 50%),
|
||||
rgba(16, 30, 24, 0.55);
|
||||
backdrop-filter: blur(24px) saturate(1.1);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(1.1);
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
backdrop-filter: blur(20px) saturate(1.1);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.1);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ===== 静态霓虹边框(卡片等普通元素) ===== */
|
||||
/* ===== 静态发光边框(卡片等普通元素) ===== */
|
||||
.neon-border {
|
||||
border: 1px solid var(--border);
|
||||
transition: border-color 200ms ease, box-shadow 200ms ease;
|
||||
transition: border-color 200ms ease, box-shadow 200ms ease, transform 200ms ease;
|
||||
}
|
||||
.neon-border:hover {
|
||||
border-color: var(--border-glow);
|
||||
box-shadow: 0 0 18px rgba(77, 184, 164, 0.08),
|
||||
inset 0 0 12px rgba(77, 184, 164, 0.03);
|
||||
box-shadow: 0 0 18px rgba(46, 139, 122, 0.08),
|
||||
inset 0 0 12px rgba(46, 139, 122, 0.03);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ===== 云烟缭绕边框 ===== */
|
||||
|
||||
/*
|
||||
* .glow-border — 选中态的云烟边框
|
||||
* 常态:石青色微弱均匀发光
|
||||
* hover:云烟缓慢缭绕,opacity 平滑过渡
|
||||
* 移开:云烟淡散回静态
|
||||
*/
|
||||
|
||||
@property --glow-angle {
|
||||
syntax: "<angle>";
|
||||
initial-value: 0deg;
|
||||
@@ -107,19 +215,16 @@ body::after {
|
||||
.glow-border {
|
||||
position: relative;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid rgba(77, 184, 164, 0.18) !important;
|
||||
box-shadow: 0 0 10px rgba(77, 184, 164, 0.05),
|
||||
inset 0 0 8px rgba(77, 184, 164, 0.02);
|
||||
border: 1px solid rgba(46, 139, 122, 0.20) !important;
|
||||
box-shadow: 0 0 10px rgba(46, 139, 122, 0.06);
|
||||
transition: border-color 400ms ease, box-shadow 400ms ease;
|
||||
z-index: 0;
|
||||
}
|
||||
.glow-border:hover {
|
||||
border-color: rgba(77, 184, 164, 0.28) !important;
|
||||
box-shadow: 0 0 16px rgba(77, 184, 164, 0.08),
|
||||
inset 0 0 12px rgba(77, 184, 164, 0.03);
|
||||
border-color: rgba(46, 139, 122, 0.32) !important;
|
||||
box-shadow: 0 0 16px rgba(46, 139, 122, 0.10);
|
||||
}
|
||||
|
||||
/* 云烟边框层 — 宽弧段柔化渐变,6s 缓慢旋转 */
|
||||
.glow-border::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -130,11 +235,11 @@ body::after {
|
||||
from var(--glow-angle),
|
||||
transparent 0%,
|
||||
transparent 15%,
|
||||
rgba(77, 184, 164, 0.2) 25%,
|
||||
rgba(58, 143, 183, 0.35) 38%,
|
||||
rgba(77, 184, 164, 0.4) 50%,
|
||||
rgba(58, 143, 183, 0.35) 62%,
|
||||
rgba(77, 184, 164, 0.2) 75%,
|
||||
rgba(46, 139, 122, 0.2) 25%,
|
||||
rgba(58, 127, 184, 0.3) 38%,
|
||||
rgba(46, 139, 122, 0.35) 50%,
|
||||
rgba(58, 127, 184, 0.3) 62%,
|
||||
rgba(46, 139, 122, 0.2) 75%,
|
||||
transparent 85%,
|
||||
transparent 100%
|
||||
);
|
||||
@@ -149,7 +254,6 @@ body::after {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* 雾气扩散层 — 更大范围、更模糊 */
|
||||
.glow-border::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -159,9 +263,9 @@ body::after {
|
||||
from var(--glow-angle),
|
||||
transparent 0%,
|
||||
transparent 20%,
|
||||
rgba(77, 184, 164, 0.06) 35%,
|
||||
rgba(58, 143, 183, 0.08) 50%,
|
||||
rgba(77, 184, 164, 0.06) 65%,
|
||||
rgba(46, 139, 122, 0.05) 35%,
|
||||
rgba(58, 127, 184, 0.06) 50%,
|
||||
rgba(46, 139, 122, 0.05) 65%,
|
||||
transparent 80%,
|
||||
transparent 100%
|
||||
);
|
||||
@@ -194,6 +298,144 @@ body::after {
|
||||
);
|
||||
}
|
||||
|
||||
/* ===== 数据流光(AI 活动时输入栏上方) ===== */
|
||||
@keyframes dataStream {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(200%); }
|
||||
}
|
||||
.data-stream {
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--accent) 30%,
|
||||
var(--accent-secondary) 50%,
|
||||
var(--accent) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: dataStream 2s ease-in-out infinite;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.data-stream-slow {
|
||||
animation-duration: 3s;
|
||||
}
|
||||
.data-stream-fast {
|
||||
animation-duration: 1.2s;
|
||||
}
|
||||
|
||||
/* ===== 水波纹 loading 动画 ===== */
|
||||
@keyframes waveRise {
|
||||
0%, 100% { transform: scaleY(0.4); }
|
||||
50% { transform: scaleY(1); }
|
||||
}
|
||||
.wave-dot {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
background: var(--accent);
|
||||
animation: waveRise 1.2s ease-in-out infinite;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
/* ===== 图片加载占位 — 石青渐变呼吸脉冲 ===== */
|
||||
@keyframes stoneBreath {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
.stone-placeholder {
|
||||
background: linear-gradient(135deg, rgba(46, 139, 122, 0.10) 0%, rgba(58, 127, 184, 0.06) 100%);
|
||||
animation: stoneBreath 2s ease-in-out infinite;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ===== shimmer 骨架屏 ===== */
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
.skeleton-shimmer {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--bg-tertiary) 0%,
|
||||
var(--bg-secondary) 40%,
|
||||
var(--bg-tertiary) 60%,
|
||||
var(--bg-tertiary) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.8s ease-in-out infinite;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ===== 涟漪效果 ===== */
|
||||
@keyframes rippleExpand {
|
||||
0% { transform: scale(0); opacity: 0.4; }
|
||||
100% { transform: scale(2.5); opacity: 0; }
|
||||
}
|
||||
.ripple-ring {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--accent);
|
||||
animation: rippleExpand 600ms ease-out forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ===== 水墨晕染揭示 ===== */
|
||||
@keyframes inkReveal {
|
||||
0% { clip-path: circle(0% at 50% 50%); }
|
||||
100% { clip-path: circle(75% at 50% 50%); }
|
||||
}
|
||||
.ink-reveal {
|
||||
animation: inkReveal 600ms ease-out forwards;
|
||||
}
|
||||
|
||||
/* ===== 错误状态震动 ===== */
|
||||
@keyframes errorShake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-2px); }
|
||||
40% { transform: translateX(2px); }
|
||||
60% { transform: translateX(-2px); }
|
||||
80% { transform: translateX(2px); }
|
||||
}
|
||||
.error-shake {
|
||||
animation: errorShake 300ms ease-out;
|
||||
}
|
||||
|
||||
/* ===== 呼吸脉冲(发送按钮就绪态) ===== */
|
||||
@keyframes breathPulse {
|
||||
0%, 100% { box-shadow: 0 0 8px rgba(46, 139, 122, 0.18); }
|
||||
50% { box-shadow: 0 0 20px rgba(46, 139, 122, 0.35); }
|
||||
}
|
||||
.breath-pulse {
|
||||
animation: breathPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ===== 输入框水面感应 ===== */
|
||||
@keyframes waterExpand {
|
||||
0% { transform: scaleX(0); }
|
||||
100% { transform: scaleX(1); }
|
||||
}
|
||||
.water-focus-line {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent 0%, var(--accent) 50%, transparent 100%);
|
||||
transform: scaleX(0);
|
||||
transition: transform 300ms ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
.water-focus-active .water-focus-line {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
/* ===== 用户消息气泡 — 左上切角 ===== */
|
||||
.user-bubble {
|
||||
border-radius: 2px 16px 16px 16px;
|
||||
}
|
||||
|
||||
/* ===== 自定义滚动条 ===== */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@@ -203,11 +445,11 @@ body::after {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(77, 184, 164, 0.15);
|
||||
background: rgba(46, 139, 122, 0.15);
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(77, 184, 164, 0.28);
|
||||
background: rgba(46, 139, 122, 0.30);
|
||||
}
|
||||
|
||||
select option {
|
||||
@@ -215,6 +457,7 @@ select option {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ===== 通用动画 ===== */
|
||||
.sidebar-overlay {
|
||||
animation: fadeIn 200ms ease-out;
|
||||
}
|
||||
@@ -226,7 +469,81 @@ select option {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes floatUp {
|
||||
from { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 0; transform: translateY(-12px); }
|
||||
}
|
||||
|
||||
/* ===== 通用按钮 hover 浮起 ===== */
|
||||
.btn-hover-lift {
|
||||
transition: transform 200ms ease, box-shadow 200ms ease;
|
||||
}
|
||||
.btn-hover-lift:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* ===== 侧栏会话左侧竖线 hover ===== */
|
||||
.session-hover-line {
|
||||
position: relative;
|
||||
}
|
||||
.session-hover-line::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
width: 2px;
|
||||
background: var(--accent);
|
||||
border-radius: 1px;
|
||||
transform: scaleY(0);
|
||||
transition: transform 200ms ease-out;
|
||||
transform-origin: center;
|
||||
}
|
||||
.session-hover-line:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
/* ===== 全屏加载 — 云雾凝聚 ===== */
|
||||
@keyframes mistCondense {
|
||||
0% { transform: scale(1.5); opacity: 0; filter: blur(20px); }
|
||||
50% { transform: scale(1.1); opacity: 0.6; filter: blur(8px); }
|
||||
100% { transform: scale(1); opacity: 1; filter: blur(0); }
|
||||
}
|
||||
@keyframes mistRing {
|
||||
0% { transform: scale(0.3); opacity: 0; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { transform: scale(2); opacity: 0; }
|
||||
}
|
||||
.mist-loader {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.mist-loader-ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--accent);
|
||||
animation: mistRing 2s ease-out infinite;
|
||||
opacity: 0;
|
||||
}
|
||||
.mist-loader-ring-2 {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
.mist-loader-core {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 20px rgba(46, 139, 122, 0.4), 0 0 60px rgba(46, 139, 122, 0.15);
|
||||
animation: mistCondense 1.5s ease-out forwards, breathPulse 2s ease-in-out 1.5s infinite;
|
||||
}
|
||||
|
||||
/* ===== 100dvh 兼容 ===== */
|
||||
@supports (height: 100dvh) {
|
||||
.h-screen {
|
||||
height: 100dvh;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, type FormEvent } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
import { AmbientParticles } from "@/components/ui/ambient-particles";
|
||||
import { staggerContainer, staggerItem, buttonTap, errorFloat } from "@/components/ui/motion-presets";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { login, isAuthenticated } = useAuth();
|
||||
@@ -32,6 +35,7 @@ export default function LoginPage() {
|
||||
router.replace("/");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "登录失败");
|
||||
setTimeout(() => setError(""), 4000);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -39,24 +43,19 @@ export default function LoginPage() {
|
||||
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-[var(--bg-primary)] relative">
|
||||
{/* 背景环境光 */}
|
||||
<div className="fixed inset-0 pointer-events-none">
|
||||
<div className="absolute inset-0"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(ellipse at 30% 70%, rgba(77, 184, 164, 0.08) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 70% 30%, rgba(58, 143, 183, 0.06) 0%, transparent 50%)
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<AmbientParticles count={25} />
|
||||
|
||||
<div className="w-full max-w-sm mx-4 relative z-10">
|
||||
<div className="text-center mb-8">
|
||||
<motion.div
|
||||
className="w-full max-w-sm mx-4 relative z-10"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<motion.div variants={staggerItem} className="text-center mb-8">
|
||||
{/* Logo 发光 */}
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl mb-4
|
||||
bg-[var(--accent)]/10 border border-[var(--accent)]/20
|
||||
shadow-[0_0_40px_rgba(77,184,164,0.15)]">
|
||||
shadow-[0_0_40px_rgba(46,139,122,0.15)]">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 22L12.5 10h2.2L19 22h-2.3l-1.1-3h-4.2l-1.1 3H8Zm3.7-5h3.1l-1.5-4.6h-.1L11.7 17Z" fill="var(--accent)" />
|
||||
<circle cx="23" cy="12" r="3.5" stroke="var(--accent)" strokeWidth="1.8" fill="none" />
|
||||
@@ -64,12 +63,15 @@ export default function LoginPage() {
|
||||
<circle cx="23" cy="22.5" r="1" fill="var(--accent)" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">EPEEKit</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-1.5">AI 美术资源生成工具</p>
|
||||
</div>
|
||||
<h1 className="typo-display mb-1">EPEEKit</h1>
|
||||
<p className="typo-caption mt-1.5">AI 美术资源生成工具</p>
|
||||
</motion.div>
|
||||
|
||||
<form onSubmit={handleSubmit}
|
||||
className="glass-panel rounded-2xl p-6 space-y-4 shadow-xl shadow-black/20">
|
||||
<motion.form
|
||||
variants={staggerItem}
|
||||
onSubmit={handleSubmit}
|
||||
className="surface-2 rounded-2xl p-6 space-y-4"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
@@ -82,7 +84,7 @@ export default function LoginPage() {
|
||||
bg-[var(--bg-primary)] border border-[var(--border)]
|
||||
text-[var(--text-primary)] placeholder:text-[var(--text-secondary)]
|
||||
focus:outline-none focus:border-[var(--accent)]/50
|
||||
focus:shadow-[0_0_10px_rgba(77,184,164,0.06)]
|
||||
focus:shadow-[0_0_10px_rgba(46,139,122,0.06)]
|
||||
transition-all"
|
||||
/>
|
||||
</div>
|
||||
@@ -97,32 +99,42 @@ export default function LoginPage() {
|
||||
bg-[var(--bg-primary)] border border-[var(--border)]
|
||||
text-[var(--text-primary)] placeholder:text-[var(--text-secondary)]
|
||||
focus:outline-none focus:border-[var(--accent)]/50
|
||||
focus:shadow-[0_0_10px_rgba(77,184,164,0.06)]
|
||||
focus:shadow-[0_0_10px_rgba(46,139,122,0.06)]
|
||||
transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm text-[var(--hot)] bg-[var(--hot)]/10 rounded-xl px-4 py-2.5
|
||||
border border-[var(--hot)]/20">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{error && (
|
||||
<motion.div
|
||||
key="login-error"
|
||||
initial="visible"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
variants={errorFloat}
|
||||
className="text-sm text-[var(--hot)] bg-[var(--hot)]/10 rounded-xl px-4 py-2.5
|
||||
border border-[var(--hot)]/20 error-shake"
|
||||
>
|
||||
{error}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<button
|
||||
<motion.button
|
||||
type="submit"
|
||||
disabled={loading || !username.trim() || !password}
|
||||
whileTap={buttonTap}
|
||||
className="w-full py-3 rounded-xl font-semibold
|
||||
bg-[var(--accent)] text-[var(--bg-primary)]
|
||||
hover:bg-[var(--accent-hover)]
|
||||
hover:shadow-[0_0_20px_rgba(77,184,164,0.25)]
|
||||
hover:shadow-[0_0_20px_rgba(46,139,122,0.25)]
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition-all cursor-pointer"
|
||||
transition-all cursor-pointer btn-hover-lift"
|
||||
>
|
||||
{loading ? "登录中..." : "登录"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</motion.button>
|
||||
</motion.form>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState, type DragEvent } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { ChatMessages } from "@/components/chat/chat-messages";
|
||||
import { ChatInput, type ChatInputHandle } from "@/components/chat/chat-input";
|
||||
import { Sidebar } from "@/components/sidebar/sidebar";
|
||||
import { TopNav } from "@/components/layout/top-nav";
|
||||
import { ImageDetailPanel } from "@/components/detail/image-detail-panel";
|
||||
import { AmbientParticles } from "@/components/ui/ambient-particles";
|
||||
import { useApp } from "@/lib/app-context";
|
||||
import { sendChat, getImageUrl, uploadRefImage } from "@/lib/api";
|
||||
import { generateId } from "@/lib/store";
|
||||
import { scrollBtnEnter, scrollBtnClick, staggerContainer, staggerItem } from "@/components/ui/motion-presets";
|
||||
import type { ChatMessage, ImageAsset, ApiMessage, AnnotationData } from "@/lib/types";
|
||||
|
||||
export default function Home() {
|
||||
@@ -29,6 +32,7 @@ export default function Home() {
|
||||
const [streamingText, setStreamingText] = useState("");
|
||||
const [streamingImages, setStreamingImages] = useState<ImageAsset[]>([]);
|
||||
const [statusText, setStatusText] = useState("");
|
||||
const [isGeneratingImage, setIsGeneratingImage] = useState(false);
|
||||
const [pendingAnnotation, setPendingAnnotation] = useState<AnnotationData | null>(null);
|
||||
const [lastRefImageUrls, setLastRefImageUrls] = useState<string[]>([]);
|
||||
const lastRefPerSession = useRef<Map<string, string[]>>(new Map());
|
||||
@@ -57,7 +61,6 @@ export default function Home() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 实时记录当前会话的滚动位置 + 判断是否显示"回到底部"按钮
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (!el || !activeSessionId) return;
|
||||
@@ -72,7 +75,6 @@ export default function Home() {
|
||||
return () => el.removeEventListener("scroll", handler);
|
||||
}, [activeSessionId]);
|
||||
|
||||
// 会话切换:标记 switching,等 DOM 更新后恢复位置 + 恢复参考图状态
|
||||
useEffect(() => {
|
||||
if (!activeSessionId) return;
|
||||
if (prevSessionId.current && prevSessionId.current !== activeSessionId) {
|
||||
@@ -152,6 +154,7 @@ export default function Home() {
|
||||
};
|
||||
appendMessage(activeSessionId, userMessage);
|
||||
setIsLoading(true);
|
||||
setIsGeneratingImage(false);
|
||||
setStreamingText("");
|
||||
setStreamingImages([]);
|
||||
setStatusText("");
|
||||
@@ -179,6 +182,7 @@ export default function Home() {
|
||||
|
||||
case "tool_start":
|
||||
setStatusText(event.data.message as string);
|
||||
setIsGeneratingImage(true);
|
||||
scrollToBottom();
|
||||
break;
|
||||
|
||||
@@ -207,6 +211,7 @@ export default function Home() {
|
||||
|
||||
collectedImages = [...collectedImages, ...newAssets];
|
||||
setStreamingImages([...collectedImages]);
|
||||
setIsGeneratingImage(false);
|
||||
if (modelName) {
|
||||
usedModelName = modelName;
|
||||
setStatusText(`由 ${modelName} 生成`);
|
||||
@@ -225,6 +230,7 @@ export default function Home() {
|
||||
assistantText += `\n\n⚠️ 图片生成失败${modelHint}:\n${errorDetail}`;
|
||||
setStreamingText(assistantText);
|
||||
setStatusText("");
|
||||
setIsGeneratingImage(false);
|
||||
scrollToBottom();
|
||||
break;
|
||||
}
|
||||
@@ -256,6 +262,7 @@ export default function Home() {
|
||||
};
|
||||
appendMessage(activeSessionId, assistantMessage);
|
||||
setIsLoading(false);
|
||||
setIsGeneratingImage(false);
|
||||
setStreamingText("");
|
||||
setStreamingImages([]);
|
||||
setStatusText("");
|
||||
@@ -268,8 +275,13 @@ export default function Home() {
|
||||
setPendingAnnotation(data);
|
||||
}, []);
|
||||
|
||||
const isEmptyState = messages.length === 0 && !isLoading;
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col relative z-[1]">
|
||||
{/* 环境粒子(萤火 + 云雾) */}
|
||||
<AmbientParticles count={isEmptyState ? 28 : 18} />
|
||||
|
||||
<TopNav />
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
@@ -323,7 +335,7 @@ export default function Home() {
|
||||
border border-[var(--border)]
|
||||
text-[var(--text-secondary)] hover:text-[var(--accent)]
|
||||
hover:border-[var(--accent)]/40
|
||||
transition-all cursor-pointer
|
||||
transition-all cursor-pointer btn-hover-lift
|
||||
hidden md:flex"
|
||||
title="展开侧边栏"
|
||||
>
|
||||
@@ -341,7 +353,7 @@ export default function Home() {
|
||||
src={pendingAnnotation.snapshot}
|
||||
alt="标注预览"
|
||||
className="w-10 h-10 rounded-lg object-cover border border-[var(--accent)]/40
|
||||
shadow-[0_0_8px_rgba(77,184,164,0.12)]"
|
||||
shadow-[0_0_8px_rgba(46,139,122,0.12)]"
|
||||
/>
|
||||
<span className="text-xs text-[var(--accent)]">
|
||||
标注已就绪({pendingAnnotation.annotations.length} 处)— 输入修改意见后发送
|
||||
@@ -356,83 +368,103 @@ export default function Home() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto fog-scroll">
|
||||
{messages.length === 0 && !isLoading ? (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center space-y-4">
|
||||
{/* Logo 发光效果 */}
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl
|
||||
bg-[var(--accent)]/10 border border-[var(--accent)]/20
|
||||
shadow-[0_0_30px_rgba(77,184,164,0.12)]">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 22L12.5 10h2.2L19 22h-2.3l-1.1-3h-4.2l-1.1 3H8Zm3.7-5h3.1l-1.5-4.6h-.1L11.7 17Z" fill="var(--accent)" />
|
||||
<circle cx="23" cy="12" r="3.5" stroke="var(--accent)" strokeWidth="1.8" fill="none" />
|
||||
<path d="M23 15.5v5" stroke="var(--accent)" strokeWidth="1.8" strokeLinecap="round" />
|
||||
<circle cx="23" cy="22.5" r="1" fill="var(--accent)" />
|
||||
</svg>
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto fog-scroll relative">
|
||||
<div className="min-h-full">
|
||||
{isEmptyState ? (
|
||||
<div className="h-full flex items-center justify-center" style={{ minHeight: "calc(100vh - 200px)" }}>
|
||||
<motion.div
|
||||
className="text-center space-y-5"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
{/* Logo 发光效果 */}
|
||||
<motion.div variants={staggerItem} className="inline-flex items-center justify-center w-16 h-16 rounded-2xl
|
||||
bg-[var(--accent)]/10 border border-[var(--accent)]/20
|
||||
shadow-[0_0_30px_rgba(46,139,122,0.12)]">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 22L12.5 10h2.2L19 22h-2.3l-1.1-3h-4.2l-1.1 3H8Zm3.7-5h3.1l-1.5-4.6h-.1L11.7 17Z" fill="var(--accent)" />
|
||||
<circle cx="23" cy="12" r="3.5" stroke="var(--accent)" strokeWidth="1.8" fill="none" />
|
||||
<path d="M23 15.5v5" stroke="var(--accent)" strokeWidth="1.8" strokeLinecap="round" />
|
||||
<circle cx="23" cy="22.5" r="1" fill="var(--accent)" />
|
||||
</svg>
|
||||
</motion.div>
|
||||
<motion.div variants={staggerItem}>
|
||||
<h2 className="typo-display mb-2">
|
||||
欢迎使用 EPEEKit
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--text-secondary)] max-w-md leading-relaxed mx-auto">
|
||||
描述你想要的美术资源,我来帮你生成。
|
||||
<br />
|
||||
你可以上传参考图来引导风格方向。
|
||||
</p>
|
||||
</motion.div>
|
||||
<motion.div variants={staggerItem} className="flex flex-wrap justify-center gap-2 pt-2">
|
||||
{[
|
||||
"画一个赛博朋克风格的退出按钮",
|
||||
"设计一个卡通风格的金币图标",
|
||||
"画一个奇幻风格的游戏角色立绘",
|
||||
].map((hint) => (
|
||||
<button
|
||||
key={hint}
|
||||
onClick={() => handleSend(hint, [], null)}
|
||||
className="text-xs px-4 py-2 rounded-xl
|
||||
border border-[var(--border)] text-[var(--text-secondary)]
|
||||
hover:border-[var(--accent)]/50 hover:text-[var(--accent)]
|
||||
hover:bg-[var(--accent)]/5
|
||||
transition-all cursor-pointer btn-hover-lift"
|
||||
>
|
||||
{hint}
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
|
||||
欢迎使用 EPEEKit
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--text-secondary)] max-w-md leading-relaxed">
|
||||
描述你想要的美术资源,我来帮你生成。
|
||||
<br />
|
||||
你可以上传参考图来引导风格方向。
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2 mt-5">
|
||||
{[
|
||||
"画一个赛博朋克风格的退出按钮",
|
||||
"设计一个卡通风格的金币图标",
|
||||
"画一个奇幻风格的游戏角色立绘",
|
||||
].map((hint) => (
|
||||
<button
|
||||
key={hint}
|
||||
onClick={() => handleSend(hint, [], null)}
|
||||
className="text-xs px-4 py-2 rounded-xl
|
||||
border border-[var(--border)] text-[var(--text-secondary)]
|
||||
hover:border-[var(--accent)]/50 hover:text-[var(--accent)]
|
||||
hover:bg-[var(--accent)]/5
|
||||
transition-all cursor-pointer"
|
||||
>
|
||||
{hint}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ChatMessages
|
||||
messages={messages}
|
||||
isLoading={isLoading}
|
||||
streamingText={streamingText}
|
||||
streamingImages={streamingImages}
|
||||
statusText={statusText}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ChatMessages
|
||||
messages={messages}
|
||||
isLoading={isLoading}
|
||||
streamingText={streamingText}
|
||||
streamingImages={streamingImages}
|
||||
statusText={statusText}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showScrollBtn && (
|
||||
<button
|
||||
onClick={() => scrollToBottom()}
|
||||
className="absolute bottom-16 right-4 z-10
|
||||
w-9 h-9 rounded-xl flex items-center justify-center
|
||||
glass-panel text-[var(--text-secondary)]
|
||||
hover:text-[var(--accent)] hover:border-[var(--accent)]/40
|
||||
shadow-lg shadow-black/30
|
||||
transition-all duration-200 cursor-pointer
|
||||
animate-[fadeIn_150ms_ease-out]"
|
||||
title="回到底部"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{/* 回到底部 — 落叶归根(水滴形) */}
|
||||
<AnimatePresence>
|
||||
{showScrollBtn && (
|
||||
<motion.button
|
||||
key="scroll-btn"
|
||||
variants={scrollBtnEnter}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
onClick={() => {
|
||||
scrollToBottom();
|
||||
}}
|
||||
className="absolute bottom-16 right-4 z-10
|
||||
w-9 h-11 flex items-center justify-center
|
||||
surface-2 text-[var(--text-secondary)]
|
||||
hover:text-[var(--accent)] hover:border-[var(--accent)]/40
|
||||
shadow-lg shadow-black/30
|
||||
cursor-pointer"
|
||||
style={{ borderRadius: "50% 50% 50% 50% / 35% 35% 65% 65%" }}
|
||||
title="回到底部"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="mt-1">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<ChatInput
|
||||
ref={chatInputRef}
|
||||
onSend={handleSend}
|
||||
disabled={isLoading}
|
||||
isGeneratingImage={isGeneratingImage}
|
||||
lastRefServerUrls={lastRefImageUrls}
|
||||
lastRefPreviewUrls={lastRefImageUrls.map((u) => getImageUrl(u))}
|
||||
onClearLastRefImage={() => {
|
||||
|
||||
Reference in New Issue
Block a user