加了一堆模型和一堆功能
This commit is contained in:
@@ -85,7 +85,7 @@ export default function GalleryPage() {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
// 忽略单张下载失败
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -109,17 +109,29 @@ export default function GalleryPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col">
|
||||
<div className="h-screen flex flex-col relative z-[1]">
|
||||
<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)] px-3 md:px-4 py-2
|
||||
<div className="flex-shrink-0 border-b border-[var(--border)] bg-[var(--bg-secondary)]/80
|
||||
backdrop-blur-xl px-4 md:px-5 py-3
|
||||
flex items-center gap-2 md:gap-3 flex-wrap">
|
||||
{/* 区块标题 */}
|
||||
<div className="flex items-center gap-2 mr-3">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2">
|
||||
<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>
|
||||
<span className="text-sm font-semibold text-[var(--text-primary)]">资源库</span>
|
||||
</div>
|
||||
|
||||
{/* 搜索 */}
|
||||
<div className="flex items-center gap-1.5 flex-1 min-w-[140px] md:min-w-[200px] max-w-[360px]">
|
||||
<div className="flex items-center gap-1.5 flex-1 min-w-[140px] md: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" />
|
||||
@@ -128,14 +140,14 @@ export default function GalleryPage() {
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索 Prompt..."
|
||||
className="flex-1 px-2 py-1 text-sm bg-transparent border-none
|
||||
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-1 flex-wrap">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{tags.map((tag) => {
|
||||
const active = selectedTagIds.includes(tag.id);
|
||||
return (
|
||||
@@ -146,10 +158,10 @@ export default function GalleryPage() {
|
||||
active ? prev.filter((id) => id !== tag.id) : [...prev, tag.id]
|
||||
)
|
||||
}
|
||||
className="px-2 py-0.5 text-[10px] rounded-full cursor-pointer border transition-colors"
|
||||
className="px-2.5 py-1 text-[11px] rounded-lg cursor-pointer border transition-all font-medium"
|
||||
style={{
|
||||
backgroundColor: active ? tag.color + "22" : "transparent",
|
||||
borderColor: active ? tag.color : "var(--border)",
|
||||
backgroundColor: active ? tag.color + "18" : "transparent",
|
||||
borderColor: active ? tag.color + "60" : "var(--border)",
|
||||
color: active ? tag.color : "var(--text-secondary)",
|
||||
}}
|
||||
>
|
||||
@@ -164,11 +176,12 @@ export default function GalleryPage() {
|
||||
{/* 收藏筛选 */}
|
||||
<button
|
||||
onClick={() => setShowFavOnly(!showFavOnly)}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs rounded cursor-pointer transition-colors"
|
||||
style={{
|
||||
color: showFavOnly ? "#f59e0b" : "var(--text-secondary)",
|
||||
backgroundColor: showFavOnly ? "#f59e0b15" : "transparent",
|
||||
}}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg cursor-pointer
|
||||
transition-all border ${
|
||||
showFavOnly
|
||||
? "border-[var(--accent)]/30 bg-[var(--accent)]/8 text-[var(--accent)]"
|
||||
: "border-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||
}`}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill={showFavOnly ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
||||
@@ -180,19 +193,22 @@ export default function GalleryPage() {
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as SortBy)}
|
||||
className="text-xs px-2 py-1 rounded bg-[var(--bg-tertiary)] border border-[var(--border)]
|
||||
text-[var(--text-secondary)] cursor-pointer focus:outline-none"
|
||||
className="text-xs px-2.5 py-1.5 rounded-lg bg-[var(--bg-tertiary)] border border-[var(--border)]
|
||||
text-[var(--text-secondary)] cursor-pointer focus:outline-none
|
||||
focus:border-[var(--accent)]/40"
|
||||
>
|
||||
<option value="time">按时间</option>
|
||||
<option value="name">按名称</option>
|
||||
</select>
|
||||
|
||||
{/* 视图切换 */}
|
||||
<div className="flex items-center gap-0.5 bg-[var(--bg-tertiary)] rounded p-0.5">
|
||||
<div className="flex items-center gap-0.5 bg-[var(--bg-tertiary)] rounded-lg p-0.5 border border-[var(--border)]">
|
||||
<button
|
||||
onClick={() => setViewMode("grid")}
|
||||
className={`p-1 rounded cursor-pointer transition-colors ${
|
||||
viewMode === "grid" ? "bg-[var(--bg-primary)] text-[var(--text-primary)]" : "text-[var(--text-secondary)]"
|
||||
className={`p-1.5 rounded-md cursor-pointer transition-all ${
|
||||
viewMode === "grid"
|
||||
? "bg-[var(--accent)]/10 text-[var(--accent)]"
|
||||
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||
}`}
|
||||
title="网格视图"
|
||||
>
|
||||
@@ -203,8 +219,10 @@ export default function GalleryPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("list")}
|
||||
className={`p-1 rounded cursor-pointer transition-colors ${
|
||||
viewMode === "list" ? "bg-[var(--bg-primary)] text-[var(--text-primary)]" : "text-[var(--text-secondary)]"
|
||||
className={`p-1.5 rounded-md cursor-pointer transition-all ${
|
||||
viewMode === "list"
|
||||
? "bg-[var(--accent)]/10 text-[var(--accent)]"
|
||||
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||
}`}
|
||||
title="列表视图"
|
||||
>
|
||||
@@ -217,11 +235,11 @@ export default function GalleryPage() {
|
||||
|
||||
{/* 批量操作栏 */}
|
||||
{selectedIds.size > 0 && (
|
||||
<div className="flex-shrink-0 px-4 py-2 bg-[var(--accent)]/10 border-b border-[var(--accent)]/30
|
||||
flex items-center gap-3">
|
||||
<div className="flex-shrink-0 px-5 py-2.5 bg-[var(--accent)]/5 border-b border-[var(--accent)]/20
|
||||
flex items-center gap-3 backdrop-blur-sm">
|
||||
<button
|
||||
onClick={selectAll}
|
||||
className="text-xs text-[var(--accent)] cursor-pointer"
|
||||
className="text-xs text-[var(--accent)] cursor-pointer hover:underline"
|
||||
>
|
||||
{selectedIds.size === filtered.length ? "取消全选" : "全选"}
|
||||
</button>
|
||||
@@ -231,15 +249,19 @@ export default function GalleryPage() {
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
onClick={handleBatchDownload}
|
||||
className="px-2.5 py-1 text-xs rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)]
|
||||
hover:text-[var(--text-primary)] border border-[var(--border)] cursor-pointer"
|
||||
className="px-3 py-1.5 text-xs rounded-lg bg-[var(--bg-tertiary)]
|
||||
text-[var(--text-secondary)] hover:text-[var(--accent)]
|
||||
border border-[var(--border)] hover:border-[var(--accent)]/40
|
||||
cursor-pointer transition-all"
|
||||
>
|
||||
批量下载
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBatchDelete}
|
||||
className="px-2.5 py-1 text-xs rounded bg-[var(--bg-tertiary)] text-red-400
|
||||
hover:text-red-300 border border-[var(--border)] cursor-pointer"
|
||||
className="px-3 py-1.5 text-xs rounded-lg bg-[var(--bg-tertiary)]
|
||||
text-[var(--hot)]/70 hover:text-[var(--hot)]
|
||||
border border-[var(--border)] hover:border-[var(--hot)]/40
|
||||
cursor-pointer transition-all"
|
||||
>
|
||||
批量删除
|
||||
</button>
|
||||
@@ -247,45 +269,53 @@ export default function GalleryPage() {
|
||||
)}
|
||||
|
||||
{/* 图片内容区 */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<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-2">
|
||||
<div className="text-3xl">📁</div>
|
||||
<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">
|
||||
<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)]">
|
||||
{assets.length === 0 ? "还没有生成过图片" : "没有匹配的资源"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : viewMode === "grid" ? (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2 md:gap-3">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3 md:gap-4">
|
||||
{filtered.map((asset) => (
|
||||
<div
|
||||
key={asset.id}
|
||||
className={`group relative rounded-lg overflow-hidden border transition-colors cursor-pointer ${
|
||||
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)]"
|
||||
: "border-[var(--border)] hover:border-[var(--border)]/80"
|
||||
? "!border-[var(--accent)] ring-1 ring-[var(--accent)]/30 shadow-[0_0_15px_rgba(77,184,164,0.08)]"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setDetailAsset(asset)}
|
||||
>
|
||||
{/* 选择框 */}
|
||||
<div
|
||||
className="absolute top-2 left-2 z-10"
|
||||
className="absolute top-2.5 left-2.5 z-10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleSelect(asset.id);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
|
||||
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)]"
|
||||
: "border-white/50 bg-black/30 opacity-100 md:opacity-0 md:group-hover:opacity-100"
|
||||
? "bg-[var(--accent)] border-[var(--accent)] shadow-[0_0_8px_rgba(77,184,164,0.25)]"
|
||||
: "border-white/40 bg-black/30 backdrop-blur-sm opacity-100 md:opacity-0 md:group-hover:opacity-100"
|
||||
}`}
|
||||
>
|
||||
{selectedIds.has(asset.id) && (
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="var(--bg-primary)" strokeWidth="3">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
)}
|
||||
@@ -295,24 +325,25 @@ export default function GalleryPage() {
|
||||
<img
|
||||
src={getImageUrl(asset.url)}
|
||||
alt={asset.prompt.slice(0, 50)}
|
||||
className="w-full aspect-square object-cover"
|
||||
className="w-full aspect-square object-cover
|
||||
transition-transform duration-300 group-hover:scale-[1.03]"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className="p-2 bg-[var(--bg-secondary)]">
|
||||
<div className="text-[10px] text-[var(--text-secondary)] truncate">
|
||||
<div className="p-2.5 bg-[var(--bg-secondary)]/80 backdrop-blur-sm">
|
||||
<div className="text-[11px] text-[var(--text-secondary)] truncate">
|
||||
{asset.prompt || "无 prompt"}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-1 flex-wrap">
|
||||
<div className="flex items-center gap-1 mt-1.5 flex-wrap">
|
||||
{asset.tags.slice(0, 2).map((tid) => {
|
||||
const tag = tagMap.get(tid);
|
||||
if (!tag) return null;
|
||||
return (
|
||||
<span
|
||||
key={tid}
|
||||
className="text-[9px] px-1 py-px rounded-full"
|
||||
style={{ backgroundColor: tag.color + "22", color: tag.color }}
|
||||
className="text-[9px] px-1.5 py-0.5 rounded-md font-medium"
|
||||
style={{ backgroundColor: tag.color + "18", color: tag.color }}
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
@@ -324,16 +355,17 @@ export default function GalleryPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮:移动端始终可见 */}
|
||||
<div className="absolute top-2 right-2 flex gap-1
|
||||
{/* 操作按钮 */}
|
||||
<div className="absolute top-2.5 right-2.5 flex gap-1.5
|
||||
opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDownload(asset);
|
||||
}}
|
||||
className="w-7 h-7 rounded bg-black/60 text-white/80 hover:text-white
|
||||
flex items-center justify-center cursor-pointer"
|
||||
className="w-7 h-7 rounded-lg bg-black/50 backdrop-blur-sm text-white/70
|
||||
hover:text-[var(--accent)] hover:bg-black/70
|
||||
flex items-center justify-center cursor-pointer transition-colors"
|
||||
title="下载"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
@@ -345,8 +377,9 @@ export default function GalleryPage() {
|
||||
e.stopPropagation();
|
||||
toggleFavorite(asset.id);
|
||||
}}
|
||||
className="w-7 h-7 rounded bg-black/60 flex items-center justify-center cursor-pointer"
|
||||
style={{ color: asset.favorited ? "#f59e0b" : "rgba(255,255,255,0.6)" }}
|
||||
className="w-7 h-7 rounded-lg bg-black/50 backdrop-blur-sm
|
||||
flex items-center justify-center cursor-pointer transition-colors"
|
||||
style={{ color: asset.favorited ? "var(--accent)" : "rgba(255,255,255,0.5)" }}
|
||||
title={asset.favorited ? "取消收藏" : "收藏"}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill={asset.favorited ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2">
|
||||
@@ -358,15 +391,14 @@ export default function GalleryPage() {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* 列表视图 */
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1.5">
|
||||
{filtered.map((asset) => (
|
||||
<div
|
||||
key={asset.id}
|
||||
className={`flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-colors ${
|
||||
className={`flex items-center gap-3 px-4 py-2.5 rounded-xl cursor-pointer transition-all ${
|
||||
selectedIds.has(asset.id)
|
||||
? "bg-[var(--accent)]/10"
|
||||
: "hover:bg-[var(--bg-tertiary)]"
|
||||
? "bg-[var(--accent)]/8 border border-[var(--accent)]/25"
|
||||
: "hover:bg-[var(--bg-tertiary)] border border-transparent"
|
||||
}`}
|
||||
onClick={() => setDetailAsset(asset)}
|
||||
>
|
||||
@@ -377,14 +409,14 @@ export default function GalleryPage() {
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`w-4 h-4 rounded border flex items-center justify-center ${
|
||||
className={`w-4 h-4 rounded-md border flex items-center justify-center transition-all ${
|
||||
selectedIds.has(asset.id)
|
||||
? "bg-[var(--accent)] border-[var(--accent)]"
|
||||
: "border-[var(--border)]"
|
||||
}`}
|
||||
>
|
||||
{selectedIds.has(asset.id) && (
|
||||
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3">
|
||||
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="var(--bg-primary)" strokeWidth="3">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
)}
|
||||
@@ -394,7 +426,7 @@ export default function GalleryPage() {
|
||||
<img
|
||||
src={getImageUrl(asset.url)}
|
||||
alt=""
|
||||
className="w-10 h-10 rounded object-cover border border-[var(--border)]"
|
||||
className="w-10 h-10 rounded-lg object-cover border border-[var(--border)]"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
@@ -409,8 +441,8 @@ export default function GalleryPage() {
|
||||
return (
|
||||
<span
|
||||
key={tid}
|
||||
className="text-[9px] px-1 py-px rounded-full"
|
||||
style={{ backgroundColor: tag.color + "22", color: tag.color }}
|
||||
className="text-[9px] px-1.5 py-0.5 rounded-md font-medium"
|
||||
style={{ backgroundColor: tag.color + "18", color: tag.color }}
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
@@ -428,8 +460,8 @@ export default function GalleryPage() {
|
||||
e.stopPropagation();
|
||||
toggleFavorite(asset.id);
|
||||
}}
|
||||
className="cursor-pointer flex-shrink-0"
|
||||
style={{ color: asset.favorited ? "#f59e0b" : "var(--text-secondary)" }}
|
||||
className="cursor-pointer flex-shrink-0 transition-colors"
|
||||
style={{ color: asset.favorited ? "var(--accent)" : "var(--text-secondary)" }}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill={asset.favorited ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
||||
@@ -441,7 +473,7 @@ export default function GalleryPage() {
|
||||
e.stopPropagation();
|
||||
handleDownload(asset);
|
||||
}}
|
||||
className="cursor-pointer text-[var(--text-secondary)] hover:text-[var(--text-primary)] flex-shrink-0"
|
||||
className="cursor-pointer text-[var(--text-secondary)] hover:text-[var(--accent)] flex-shrink-0 transition-colors"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" />
|
||||
@@ -473,7 +505,6 @@ export default function GalleryPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// 资源详情 Modal
|
||||
function GalleryDetailModal({
|
||||
asset,
|
||||
tags,
|
||||
@@ -493,20 +524,22 @@ function GalleryDetailModal({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="bg-[var(--bg-secondary)] rounded-xl border border-[var(--border)]
|
||||
max-w-3xl w-full mx-2 md:mx-4 max-h-[90vh] md:max-h-[85vh] overflow-hidden flex flex-col"
|
||||
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"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-[var(--border)]">
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">资源详情</span>
|
||||
<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>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-[var(--text-secondary)] hover:text-[var(--text-primary)] cursor-pointer"
|
||||
className="text-[var(--text-secondary)] hover:text-[var(--accent)] cursor-pointer
|
||||
transition-colors p-1 rounded-lg hover:bg-[var(--accent)]/5"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
@@ -517,24 +550,23 @@ function GalleryDetailModal({
|
||||
{/* 内容 */}
|
||||
<div className="flex-1 overflow-y-auto p-5">
|
||||
<div className="flex gap-5 flex-col md:flex-row">
|
||||
{/* 图片 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<img
|
||||
src={getImageUrl(asset.url)}
|
||||
alt={asset.prompt}
|
||||
className="w-full rounded-lg border border-[var(--border)]"
|
||||
className="w-full rounded-xl border border-[var(--border)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 信息 */}
|
||||
<div className="w-full md:w-[260px] flex-shrink-0 space-y-4">
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={onDownload}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md
|
||||
bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)]
|
||||
cursor-pointer transition-colors"
|
||||
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)]
|
||||
cursor-pointer transition-all"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" />
|
||||
@@ -543,13 +575,12 @@ function GalleryDetailModal({
|
||||
</button>
|
||||
<button
|
||||
onClick={onToggleFavorite}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md
|
||||
border cursor-pointer transition-colors"
|
||||
style={{
|
||||
backgroundColor: asset.favorited ? "#f59e0b22" : "var(--bg-tertiary)",
|
||||
borderColor: asset.favorited ? "#f59e0b" : "var(--border)",
|
||||
color: asset.favorited ? "#f59e0b" : "var(--text-secondary)",
|
||||
}}
|
||||
className={`flex items-center gap-1.5 px-3.5 py-2 text-xs rounded-xl font-medium
|
||||
border cursor-pointer transition-all ${
|
||||
asset.favorited
|
||||
? "border-[var(--accent)]/40 bg-[var(--accent)]/8 text-[var(--accent)]"
|
||||
: "border-[var(--border)] bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||
}`}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill={asset.favorited ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
||||
@@ -558,10 +589,10 @@ function GalleryDetailModal({
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md
|
||||
bg-[var(--bg-tertiary)] text-red-400/70 hover:text-red-400
|
||||
border border-[var(--border)] hover:border-red-400/50
|
||||
cursor-pointer transition-colors"
|
||||
className="flex items-center gap-1.5 px-3.5 py-2 text-xs rounded-xl font-medium
|
||||
bg-[var(--bg-tertiary)] text-[var(--hot)]/60 hover:text-[var(--hot)]
|
||||
border border-[var(--border)] hover:border-[var(--hot)]/40
|
||||
cursor-pointer transition-all"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
@@ -570,17 +601,17 @@ function GalleryDetailModal({
|
||||
{/* Prompt */}
|
||||
{asset.prompt && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">Prompt</span>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(asset.prompt)}
|
||||
className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--accent)]
|
||||
cursor-pointer"
|
||||
cursor-pointer transition-colors"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-2 rounded bg-[var(--bg-primary)] border border-[var(--border)]
|
||||
<div className="p-3 rounded-xl bg-[var(--bg-primary)] border border-[var(--border)]
|
||||
text-xs text-[var(--text-secondary)] leading-relaxed break-all">
|
||||
{asset.prompt}
|
||||
</div>
|
||||
@@ -590,16 +621,16 @@ function GalleryDetailModal({
|
||||
{/* 标签 */}
|
||||
{asset.tags.length > 0 && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)] block mb-1">标签</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)] block mb-1.5">标签</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{asset.tags.map((tid) => {
|
||||
const tag = tagMap.get(tid);
|
||||
if (!tag) return null;
|
||||
return (
|
||||
<span
|
||||
key={tid}
|
||||
className="text-[10px] px-2 py-0.5 rounded-full"
|
||||
style={{ backgroundColor: tag.color + "22", color: tag.color }}
|
||||
className="text-[10px] px-2 py-0.5 rounded-lg font-medium"
|
||||
style={{ backgroundColor: tag.color + "18", color: tag.color }}
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
@@ -610,9 +641,9 @@ function GalleryDetailModal({
|
||||
)}
|
||||
|
||||
{/* 元信息 */}
|
||||
<div className="text-xs text-[var(--text-secondary)] space-y-1">
|
||||
<div className="text-xs text-[var(--text-secondary)] space-y-1.5">
|
||||
<div>创建时间:{new Date(asset.createdAt).toLocaleString("zh-CN")}</div>
|
||||
<div>会话 ID:{asset.sessionId}</div>
|
||||
<div className="truncate">会话 ID:{asset.sessionId}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,200 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* ===== 青绿山水色彩体系 ===== */
|
||||
:root {
|
||||
--bg-primary: #0a0a0a;
|
||||
--bg-secondary: #141414;
|
||||
--bg-tertiary: #1e1e1e;
|
||||
--text-primary: #e5e5e5;
|
||||
--text-secondary: #a3a3a3;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #818cf8;
|
||||
--border: #2e2e2e;
|
||||
--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;
|
||||
--hot: #C4654A;
|
||||
--border: rgba(77, 184, 164, 0.12);
|
||||
--border-glow: rgba(77, 184, 164, 0.25);
|
||||
--mist: rgba(77, 184, 164, 0.06);
|
||||
--gold: #B8935A;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
overscroll-behavior: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
/* ===== 山雾背景 — 远景层 ===== */
|
||||
@keyframes fogDriftFar {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
33% { transform: translate(2%, -1.5%) scale(1.02); }
|
||||
66% { transform: translate(-1.5%, 1%) scale(0.98); }
|
||||
100% { transform: translate(0, 0) scale(1); }
|
||||
}
|
||||
@keyframes fogDriftNear {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
25% { transform: translate(-3%, 2%) scale(1.03); }
|
||||
50% { transform: translate(1%, -1%) scale(0.97); }
|
||||
75% { transform: translate(2.5%, 1.5%) scale(1.01); }
|
||||
100% { transform: translate(0, 0) scale(1); }
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
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%);
|
||||
animation: fogDriftFar 60s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
body::after {
|
||||
content: '';
|
||||
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%);
|
||||
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);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ===== 静态霓虹边框(卡片等普通元素) ===== */
|
||||
.neon-border {
|
||||
border: 1px solid var(--border);
|
||||
transition: border-color 200ms ease, box-shadow 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);
|
||||
}
|
||||
|
||||
/* ===== 云烟缭绕边框 ===== */
|
||||
|
||||
/*
|
||||
* .glow-border — 选中态的云烟边框
|
||||
* 常态:石青色微弱均匀发光
|
||||
* hover:云烟缓慢缭绕,opacity 平滑过渡
|
||||
* 移开:云烟淡散回静态
|
||||
*/
|
||||
|
||||
@property --glow-angle {
|
||||
syntax: "<angle>";
|
||||
initial-value: 0deg;
|
||||
inherits: false;
|
||||
}
|
||||
@keyframes mistSpin {
|
||||
to { --glow-angle: 360deg; }
|
||||
}
|
||||
|
||||
.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);
|
||||
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);
|
||||
}
|
||||
|
||||
/* 云烟边框层 — 宽弧段柔化渐变,6s 缓慢旋转 */
|
||||
.glow-border::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: inherit;
|
||||
padding: 1.5px;
|
||||
background: conic-gradient(
|
||||
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%,
|
||||
transparent 85%,
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
animation: mistSpin 6s linear infinite;
|
||||
opacity: 0;
|
||||
transition: opacity 600ms ease;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* 雾气扩散层 — 更大范围、更模糊 */
|
||||
.glow-border::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -6px;
|
||||
border-radius: inherit;
|
||||
background: conic-gradient(
|
||||
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%,
|
||||
transparent 80%,
|
||||
transparent 100%
|
||||
);
|
||||
filter: blur(12px);
|
||||
animation: mistSpin 6s linear infinite;
|
||||
opacity: 0;
|
||||
transition: opacity 600ms ease;
|
||||
pointer-events: none;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
.glow-border:hover::before { opacity: 1; }
|
||||
.glow-border:hover::after { opacity: 1; }
|
||||
|
||||
/* ===== 雾气滚动边缘 ===== */
|
||||
.fog-scroll {
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0px,
|
||||
black 36px,
|
||||
black calc(100% - 36px),
|
||||
transparent 100%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0px,
|
||||
black 36px,
|
||||
black calc(100% - 36px),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* ===== 自定义滚动条 ===== */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@@ -28,17 +203,18 @@ body {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
background: rgba(77, 184, 164, 0.15);
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(77, 184, 164, 0.28);
|
||||
}
|
||||
|
||||
/* 原生 select 下拉框暗色 */
|
||||
select option {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 移动端侧边栏遮罩动画 */
|
||||
.sidebar-overlay {
|
||||
animation: fadeIn 200ms ease-out;
|
||||
}
|
||||
@@ -46,8 +222,11 @@ select option {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 移动端防止 iOS 地址栏 bounce */
|
||||
@supports (height: 100dvh) {
|
||||
.h-screen {
|
||||
height: 100dvh;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { useState, useEffect, type FormEvent } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
|
||||
@@ -12,8 +12,13 @@ export default function LoginPage() {
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
router.replace("/");
|
||||
}
|
||||
}, [isAuthenticated, router]);
|
||||
|
||||
if (isAuthenticated) {
|
||||
router.replace("/");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -33,15 +38,38 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-[var(--bg-primary)]">
|
||||
<div className="w-full max-w-sm mx-4">
|
||||
<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>
|
||||
|
||||
<div className="w-full max-w-sm mx-4 relative z-10">
|
||||
<div className="text-center mb-8">
|
||||
<div className="text-4xl mb-3">🎨</div>
|
||||
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">EPEEKit</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-1">AI 美术资源生成工具</p>
|
||||
{/* 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)]">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<form onSubmit={handleSubmit}
|
||||
className="glass-panel rounded-2xl p-6 space-y-4 shadow-xl shadow-black/20">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
@@ -50,11 +78,12 @@ export default function LoginPage() {
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
autoComplete="username"
|
||||
className="w-full px-4 py-3 rounded-lg
|
||||
bg-[var(--bg-secondary)] border border-[var(--border)]
|
||||
className="w-full px-4 py-3 rounded-xl
|
||||
bg-[var(--bg-primary)] border border-[var(--border)]
|
||||
text-[var(--text-primary)] placeholder:text-[var(--text-secondary)]
|
||||
focus:outline-none focus:border-[var(--accent)]
|
||||
transition-colors"
|
||||
focus:outline-none focus:border-[var(--accent)]/50
|
||||
focus:shadow-[0_0_10px_rgba(77,184,164,0.06)]
|
||||
transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -64,16 +93,18 @@ export default function LoginPage() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
className="w-full px-4 py-3 rounded-lg
|
||||
bg-[var(--bg-secondary)] border border-[var(--border)]
|
||||
className="w-full px-4 py-3 rounded-xl
|
||||
bg-[var(--bg-primary)] border border-[var(--border)]
|
||||
text-[var(--text-primary)] placeholder:text-[var(--text-secondary)]
|
||||
focus:outline-none focus:border-[var(--accent)]
|
||||
transition-colors"
|
||||
focus:outline-none focus:border-[var(--accent)]/50
|
||||
focus:shadow-[0_0_10px_rgba(77,184,164,0.06)]
|
||||
transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm text-red-400 bg-red-400/10 rounded-lg px-4 py-2.5">
|
||||
<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>
|
||||
)}
|
||||
@@ -81,11 +112,12 @@ export default function LoginPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !username.trim() || !password}
|
||||
className="w-full py-3 rounded-lg font-medium
|
||||
bg-[var(--accent)] text-white
|
||||
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)]
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition-colors cursor-pointer"
|
||||
transition-all cursor-pointer"
|
||||
>
|
||||
{loading ? "登录中..." : "登录"}
|
||||
</button>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState, type DragEvent } from "react";
|
||||
import { ChatMessages } from "@/components/chat/chat-messages";
|
||||
import { ChatInput } from "@/components/chat/chat-input";
|
||||
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";
|
||||
@@ -30,6 +30,11 @@ export default function Home() {
|
||||
const [streamingImages, setStreamingImages] = useState<ImageAsset[]>([]);
|
||||
const [statusText, setStatusText] = useState("");
|
||||
const [pendingAnnotation, setPendingAnnotation] = useState<AnnotationData | null>(null);
|
||||
const [lastRefImageUrls, setLastRefImageUrls] = useState<string[]>([]);
|
||||
const lastRefPerSession = useRef<Map<string, string[]>>(new Map());
|
||||
const [mainDragging, setMainDragging] = useState(false);
|
||||
const dragCounter = useRef(0);
|
||||
const chatInputRef = useRef<ChatInputHandle>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const scrollPositions = useRef<Map<string, number>>(new Map());
|
||||
const prevSessionId = useRef<string | null>(null);
|
||||
@@ -67,13 +72,14 @@ export default function Home() {
|
||||
return () => el.removeEventListener("scroll", handler);
|
||||
}, [activeSessionId]);
|
||||
|
||||
// 会话切换:标记 switching,等 DOM 更新后恢复位置
|
||||
// 会话切换:标记 switching,等 DOM 更新后恢复位置 + 恢复参考图状态
|
||||
useEffect(() => {
|
||||
if (!activeSessionId) return;
|
||||
if (prevSessionId.current && prevSessionId.current !== activeSessionId) {
|
||||
isSwitching.current = true;
|
||||
}
|
||||
prevSessionId.current = activeSessionId;
|
||||
setLastRefImageUrls(lastRefPerSession.current.get(activeSessionId) ?? []);
|
||||
}, [activeSessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -95,11 +101,11 @@ export default function Home() {
|
||||
}, [activeSessionId, messages.length]);
|
||||
|
||||
const handleSend = useCallback(
|
||||
async (text: string, refImageServerUrl: string | null, imageModel: string | null = null) => {
|
||||
async (text: string, refImageServerUrls: string[], imageModel: string | null = null) => {
|
||||
if (!activeSessionId || !activeSession) return;
|
||||
|
||||
let finalText = text;
|
||||
let finalRefServerUrl = refImageServerUrl;
|
||||
let finalRefServerUrls = [...refImageServerUrls];
|
||||
|
||||
if (pendingAnnotation) {
|
||||
const annotationDescs = pendingAnnotation.annotations
|
||||
@@ -116,14 +122,13 @@ export default function Home() {
|
||||
finalText = `${text}\n\n--- 图片标注 ---\n${annotationDescs}`;
|
||||
}
|
||||
|
||||
// 标注截图作为参考图:需要先上传再获取 URL
|
||||
if (pendingAnnotation.snapshot && !refImageServerUrl) {
|
||||
if (pendingAnnotation.snapshot && finalRefServerUrls.length === 0) {
|
||||
try {
|
||||
const res = await fetch(pendingAnnotation.snapshot);
|
||||
const blob = await res.blob();
|
||||
const file = new File([blob], "annotation.png", { type: "image/png" });
|
||||
const uploadResult = await uploadRefImage(file);
|
||||
finalRefServerUrl = uploadResult.url;
|
||||
finalRefServerUrls = [uploadResult.url];
|
||||
} catch {
|
||||
// 忽略转换/上传失败
|
||||
}
|
||||
@@ -132,17 +137,18 @@ export default function Home() {
|
||||
setPendingAnnotation(null);
|
||||
}
|
||||
|
||||
// 用户消息中的参考图预览:优先使用服务端路径(通过 getImageUrl 转为完整 URL)
|
||||
let previewUrl: string | undefined;
|
||||
if (finalRefServerUrl) {
|
||||
previewUrl = getImageUrl(finalRefServerUrl);
|
||||
if (finalRefServerUrls.length > 0) {
|
||||
lastRefPerSession.current.set(activeSessionId, finalRefServerUrls);
|
||||
setLastRefImageUrls(finalRefServerUrls);
|
||||
}
|
||||
|
||||
const previewUrls = finalRefServerUrls.map((u) => getImageUrl(u));
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
id: generateId("msg-"),
|
||||
role: "user",
|
||||
content: finalText,
|
||||
refImageUrl: previewUrl,
|
||||
refImageUrls: previewUrls.length > 0 ? previewUrls : undefined,
|
||||
};
|
||||
appendMessage(activeSessionId, userMessage);
|
||||
setIsLoading(true);
|
||||
@@ -160,8 +166,10 @@ export default function Home() {
|
||||
let collectedImages: ImageAsset[] = [];
|
||||
let usedModelName = "";
|
||||
|
||||
const llmModel = activeSession?.llmModel ?? null;
|
||||
|
||||
try {
|
||||
for await (const event of sendChat(apiMessages, finalRefServerUrl, imageModel, activeSessionId)) {
|
||||
for await (const event of sendChat(apiMessages, finalRefServerUrls.length > 0 ? finalRefServerUrls : null, imageModel, activeSessionId, llmModel)) {
|
||||
switch (event.type) {
|
||||
case "text_delta":
|
||||
assistantText += event.data.text as string;
|
||||
@@ -261,21 +269,61 @@ export default function Home() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col">
|
||||
<div className="h-screen flex flex-col relative z-[1]">
|
||||
<TopNav />
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<Sidebar />
|
||||
|
||||
<main className="flex-1 flex flex-col min-w-0 relative">
|
||||
{/* 桌面端侧边栏展开按钮(移动端用顶栏汉堡菜单替代) */}
|
||||
<main
|
||||
className="flex-1 flex flex-col min-w-0 relative"
|
||||
onDragEnter={(e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
dragCounter.current++;
|
||||
if (e.dataTransfer.types.includes("Files")) setMainDragging(true);
|
||||
}}
|
||||
onDragOver={(e: DragEvent) => e.preventDefault()}
|
||||
onDragLeave={(e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
dragCounter.current--;
|
||||
if (dragCounter.current <= 0) { dragCounter.current = 0; setMainDragging(false); }
|
||||
}}
|
||||
onDrop={(e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
dragCounter.current = 0;
|
||||
setMainDragging(false);
|
||||
const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith("image/"));
|
||||
files.forEach((f) => chatInputRef.current?.uploadFile(f));
|
||||
}}
|
||||
>
|
||||
{/* 拖拽覆盖层 */}
|
||||
{mainDragging && (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center
|
||||
bg-[var(--bg-primary)]/80 backdrop-blur-sm
|
||||
border-2 border-dashed border-[var(--accent)]/50 rounded-xl m-2
|
||||
pointer-events-none">
|
||||
<div className="text-center space-y-2">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none"
|
||||
stroke="var(--accent)" strokeWidth="1.5" className="mx-auto opacity-70">
|
||||
<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>
|
||||
<p className="text-sm text-[var(--accent)] font-medium">松开以添加参考图(可多张)</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 桌面端侧边栏展开按钮 */}
|
||||
{sidebarCollapsed && (
|
||||
<button
|
||||
onClick={() => setSidebarCollapsed(false)}
|
||||
className="absolute left-1 top-2 z-10 p-1.5 rounded-md
|
||||
bg-[var(--bg-secondary)] border border-[var(--border)]
|
||||
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
|
||||
transition-colors cursor-pointer
|
||||
className="absolute left-2 top-2 z-10 p-1.5 rounded-lg
|
||||
bg-[var(--bg-secondary)]/80 backdrop-blur-sm
|
||||
border border-[var(--border)]
|
||||
text-[var(--text-secondary)] hover:text-[var(--accent)]
|
||||
hover:border-[var(--accent)]/40
|
||||
transition-all cursor-pointer
|
||||
hidden md:flex"
|
||||
title="展开侧边栏"
|
||||
>
|
||||
@@ -287,12 +335,13 @@ export default function Home() {
|
||||
|
||||
{/* 标注提示条 */}
|
||||
{pendingAnnotation && (
|
||||
<div className="flex-shrink-0 px-4 py-2 bg-[var(--accent)]/10 border-b border-[var(--accent)]/30
|
||||
flex items-center gap-3">
|
||||
<div className="flex-shrink-0 px-5 py-2.5 bg-[var(--accent)]/5 border-b border-[var(--accent)]/20
|
||||
flex items-center gap-3 backdrop-blur-sm">
|
||||
<img
|
||||
src={pendingAnnotation.snapshot}
|
||||
alt="标注预览"
|
||||
className="w-10 h-10 rounded object-cover border border-[var(--accent)]"
|
||||
className="w-10 h-10 rounded-lg object-cover border border-[var(--accent)]/40
|
||||
shadow-[0_0_8px_rgba(77,184,164,0.12)]"
|
||||
/>
|
||||
<span className="text-xs text-[var(--accent)]">
|
||||
标注已就绪({pendingAnnotation.annotations.length} 处)— 输入修改意见后发送
|
||||
@@ -300,27 +349,37 @@ export default function Home() {
|
||||
<button
|
||||
onClick={() => setPendingAnnotation(null)}
|
||||
className="ml-auto text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]
|
||||
cursor-pointer"
|
||||
cursor-pointer transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto">
|
||||
<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-3">
|
||||
<div className="text-4xl">🎨</div>
|
||||
<h2 className="text-xl font-medium text-[var(--text-primary)]">
|
||||
<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>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
|
||||
欢迎使用 EPEEKit
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--text-secondary)] max-w-md">
|
||||
<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-4">
|
||||
<div className="flex flex-wrap justify-center gap-2 mt-5">
|
||||
{[
|
||||
"画一个赛博朋克风格的退出按钮",
|
||||
"设计一个卡通风格的金币图标",
|
||||
@@ -328,11 +387,12 @@ export default function Home() {
|
||||
].map((hint) => (
|
||||
<button
|
||||
key={hint}
|
||||
onClick={() => handleSend(hint, null, null)}
|
||||
className="text-xs px-3 py-1.5 rounded-full
|
||||
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)] hover:text-[var(--accent)]
|
||||
transition-colors cursor-pointer"
|
||||
hover:border-[var(--accent)]/50 hover:text-[var(--accent)]
|
||||
hover:bg-[var(--accent)]/5
|
||||
transition-all cursor-pointer"
|
||||
>
|
||||
{hint}
|
||||
</button>
|
||||
@@ -355,10 +415,10 @@ export default function Home() {
|
||||
<button
|
||||
onClick={() => scrollToBottom()}
|
||||
className="absolute bottom-16 right-4 z-10
|
||||
w-8 h-8 rounded-full flex items-center justify-center
|
||||
bg-[var(--bg-tertiary)] border border-[var(--border)]
|
||||
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
|
||||
hover:border-[var(--accent)] shadow-lg
|
||||
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="回到底部"
|
||||
@@ -369,7 +429,21 @@ export default function Home() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
<ChatInput onSend={handleSend} disabled={isLoading} />
|
||||
<ChatInput
|
||||
ref={chatInputRef}
|
||||
onSend={handleSend}
|
||||
disabled={isLoading}
|
||||
lastRefServerUrls={lastRefImageUrls}
|
||||
lastRefPreviewUrls={lastRefImageUrls.map((u) => getImageUrl(u))}
|
||||
onClearLastRefImage={() => {
|
||||
if (activeSessionId) lastRefPerSession.current.delete(activeSessionId);
|
||||
setLastRefImageUrls([]);
|
||||
}}
|
||||
onFileDrop={() => {
|
||||
dragCounter.current = 0;
|
||||
setMainDragging(false);
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
|
||||
{detailImage && (
|
||||
|
||||
Reference in New Issue
Block a user