feat: align style pro UI with index prototype
This commit is contained in:
parent
6a47359d8e
commit
fa2f26bf97
@ -16,7 +16,6 @@ import useSWR from "swr";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { imageGenerationService } from "@/services/canvas";
|
||||
import { cn, fetcher } from "@/lib/utils";
|
||||
import type {
|
||||
@ -107,7 +106,7 @@ function ImagePreview({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full min-h-[220px] items-center justify-center rounded-lg border border-dashed border-white/15 bg-white/[0.03] text-sm text-white/45",
|
||||
"flex h-full min-h-[220px] items-center justify-center rounded-lg border border-dashed border-gray-200 bg-gray-50 text-gray-400 text-sm",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@ -208,7 +207,7 @@ function BeforeAfterSlider({
|
||||
|
||||
if (!beforeUrl && !afterUrl) {
|
||||
return (
|
||||
<div className="flex min-h-[460px] items-center justify-center rounded-lg border border-dashed border-white/15 bg-white/[0.03] text-white/45">
|
||||
<div className="flex min-h-[460px] items-center justify-center rounded-lg border border-dashed border-gray-200 bg-gray-50 text-gray-400">
|
||||
上传图片后开始生成
|
||||
</div>
|
||||
);
|
||||
@ -216,14 +215,14 @@ function BeforeAfterSlider({
|
||||
|
||||
if (!afterUrl) {
|
||||
return (
|
||||
<div className="min-h-[460px] rounded-lg border border-white/10 bg-[#111] p-3">
|
||||
<div className="min-h-[460px] rounded-lg border border-gray-200 bg-gray-50 p-3">
|
||||
<ImagePreview alt="上传图片" imageUrl={beforeUrl} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative min-h-[460px] overflow-hidden rounded-lg border border-white/10 bg-[#111]">
|
||||
<div className="relative min-h-[460px] overflow-hidden rounded-lg border border-gray-200 bg-gray-50">
|
||||
<ImagePreview
|
||||
alt="生成后"
|
||||
className="absolute inset-0 h-full w-full"
|
||||
@ -507,10 +506,56 @@ export function StyleProWorkspace({
|
||||
: "";
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh w-full overflow-hidden bg-[#050606] text-white">
|
||||
<aside className="flex w-[76px] shrink-0 flex-col items-center border-white/10 border-r bg-black">
|
||||
<div className="style-pro-index-shell flex h-dvh w-full overflow-hidden bg-gray-50 text-gray-900">
|
||||
<style>{`
|
||||
.style-pro-index-shell .section-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 35px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
.style-pro-index-shell .subsection-title {
|
||||
color: #2563eb;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.style-pro-index-shell .option-btn {
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #f9fafb;
|
||||
color: #374151;
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
.style-pro-index-shell .option-btn:hover {
|
||||
background: #1f2937;
|
||||
border-color: #1f2937;
|
||||
color: #fff;
|
||||
}
|
||||
.style-pro-index-shell .option-btn.active {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(37, 99, 235, 0.05));
|
||||
color: #2563eb;
|
||||
border-color: #60a5fa;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
.style-pro-index-shell .preset-btn {
|
||||
border-radius: 9999px;
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
.style-pro-index-shell .custom-input {
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #fafafa;
|
||||
color: #111827;
|
||||
outline: none;
|
||||
}
|
||||
.style-pro-index-shell .custom-input:focus {
|
||||
border-color: #60a5fa;
|
||||
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.18);
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<aside className="flex w-[76px] shrink-0 flex-col items-center border-gray-200 border-r bg-white">
|
||||
<button
|
||||
className="mt-5 flex h-11 w-11 items-center justify-center rounded-lg bg-white/[0.06] text-white"
|
||||
className="mt-5 flex h-11 w-11 items-center justify-center rounded-lg border border-gray-200 bg-gray-50 text-gray-800"
|
||||
onClick={onBack}
|
||||
title="返回画布"
|
||||
type="button"
|
||||
@ -518,12 +563,12 @@ export function StyleProWorkspace({
|
||||
{logo ? (
|
||||
<img alt={siteName} className="h-6 w-6 object-contain" src={logo} />
|
||||
) : (
|
||||
<Sparkles className="h-5 w-5 text-emerald-300" />
|
||||
<Sparkles className="h-5 w-5 text-blue-600" />
|
||||
)}
|
||||
</button>
|
||||
<div className="mt-8 flex flex-col gap-3">
|
||||
<button
|
||||
className="flex h-11 w-11 items-center justify-center rounded-lg bg-emerald-400 text-black"
|
||||
className="flex h-11 w-11 items-center justify-center rounded-lg bg-blue-600 text-white shadow-sm"
|
||||
onClick={onBack}
|
||||
title="返回画布"
|
||||
type="button"
|
||||
@ -531,7 +576,7 @@ export function StyleProWorkspace({
|
||||
<Home className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
className="flex h-11 w-11 items-center justify-center rounded-lg bg-white/[0.05] text-white/75 hover:bg-white/[0.09] hover:text-white"
|
||||
className="flex h-11 w-11 items-center justify-center rounded-lg border border-gray-200 bg-gray-50 text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
title="上传图片"
|
||||
type="button"
|
||||
@ -541,51 +586,113 @@ export function StyleProWorkspace({
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex min-w-0 flex-1 flex-col">
|
||||
<header className="flex h-16 shrink-0 items-center justify-between border-white/10 border-b bg-black/85 px-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
className="rounded-lg border-white/10 bg-white/[0.04] text-white hover:bg-white/[0.08]"
|
||||
onClick={onBack}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
返回画布
|
||||
</Button>
|
||||
<div>
|
||||
<div className="text-sm text-white/45">{siteName}</div>
|
||||
<h1 className="font-semibold text-xl">风格化 PRO</h1>
|
||||
<main className="min-w-0 flex-1 overflow-y-auto">
|
||||
<div className="sticky top-0 z-40 border-gray-200 border-b bg-white">
|
||||
<div className="mx-auto flex max-w-[1920px] items-center justify-between gap-4 px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
className="rounded-lg border-gray-200 bg-white text-gray-700 hover:bg-gray-100"
|
||||
onClick={onBack}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
返回画布
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
{logo ? (
|
||||
<img alt={siteName} className="h-8 w-8 object-contain" src={logo} />
|
||||
) : (
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600 text-white">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900 text-sm">{siteName}</div>
|
||||
<div className="text-gray-500 text-xs">风格化 PRO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-end gap-3">
|
||||
<button
|
||||
className="preset-btn flex items-center gap-2 border-2 border-blue-400 bg-gradient-to-r from-blue-500/20 to-blue-300/5 px-5 py-2 font-bold text-blue-700 text-sm"
|
||||
onClick={quickFillRequired}
|
||||
type="button"
|
||||
>
|
||||
<UserRound className="h-4 w-4" />
|
||||
执行者
|
||||
</button>
|
||||
<button
|
||||
className="preset-btn flex items-center gap-2 border-2 border-blue-400 bg-gradient-to-r from-blue-500/20 to-blue-300/5 px-5 py-2 font-bold text-blue-700 text-sm disabled:opacity-50"
|
||||
disabled={generating || Boolean(disabledReason)}
|
||||
onClick={handleGenerate}
|
||||
type="button"
|
||||
>
|
||||
{generating ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-4 w-4" />
|
||||
)}
|
||||
执行
|
||||
</button>
|
||||
<button
|
||||
className="preset-btn flex items-center gap-2 border-2 border-blue-400 bg-gradient-to-r from-blue-500/20 to-blue-300/5 px-5 py-2 font-bold text-blue-700 text-sm"
|
||||
onClick={() => {
|
||||
setSelections({});
|
||||
setCurrentResult(null);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
重置
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-gray-700 text-sm hover:bg-gray-100"
|
||||
onClick={() => toast.info("个人中心入口已保留,可在首页打开完整账户面板")}
|
||||
type="button"
|
||||
>
|
||||
<UserRound className="h-4 w-4" />
|
||||
个人中心
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-sm text-white/80 hover:bg-white/[0.08] hover:text-white"
|
||||
onClick={() => toast.info("个人中心入口已保留,可在首页打开完整账户面板")}
|
||||
type="button"
|
||||
>
|
||||
<UserRound className="h-4 w-4" />
|
||||
个人中心
|
||||
</button>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-0 flex-1 grid-cols-[360px_minmax(0,1fr)] overflow-hidden">
|
||||
<section className="flex min-h-0 flex-col border-white/10 border-r bg-[#080909]">
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="space-y-5 p-5">
|
||||
<div>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="font-semibold text-base">上传图片</h2>
|
||||
<p className="mt-1 text-sm text-white/45">
|
||||
上传后选择按钮组合生成最终图。
|
||||
</p>
|
||||
</div>
|
||||
<Badge className="border-emerald-400/30 bg-emerald-400/10 text-emerald-200">
|
||||
Final
|
||||
</Badge>
|
||||
<div className="mx-auto max-w-[1920px] space-y-6 px-6 py-8">
|
||||
<section className="section-card p-6">
|
||||
<div className="mb-5 flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="font-black text-4xl text-gray-900 lg:text-5xl">
|
||||
风格化 PRO
|
||||
</h1>
|
||||
<p className="mt-2 text-gray-500 text-sm">
|
||||
上传图片,选择下方按钮组合,按执行生成最终图片。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge className="border-blue-200 bg-blue-50 text-blue-800">
|
||||
{selectedOptionLabels.length
|
||||
? `已选择 ${selectedOptionLabels.length} 项`
|
||||
: "等待选择"}
|
||||
</Badge>
|
||||
{disabledReason ? (
|
||||
<Badge className="border-amber-200 bg-amber-50 text-amber-700">
|
||||
{disabledReason}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[360px_minmax(0,1fr)]">
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl bg-gray-50 p-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<span className="subsection-title">上传图片</span>
|
||||
<span className="text-gray-400 text-xs">支持拖拽上传</span>
|
||||
</div>
|
||||
<button
|
||||
className="group relative flex min-h-[230px] w-full items-center justify-center overflow-hidden rounded-lg border border-dashed border-white/15 bg-white/[0.03] transition hover:border-emerald-300/60 hover:bg-white/[0.05]"
|
||||
className="flex min-h-[260px] w-full items-center justify-center overflow-hidden rounded-lg border border-dashed border-gray-300 bg-white transition hover:border-blue-400"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDrop={(event) => {
|
||||
@ -597,19 +704,17 @@ export function StyleProWorkspace({
|
||||
{sourceImageUrl ? (
|
||||
<img
|
||||
alt="上传图片"
|
||||
className="h-full max-h-[300px] w-full object-contain"
|
||||
className="h-full max-h-[320px] w-full object-contain"
|
||||
src={sourceImageUrl}
|
||||
/>
|
||||
) : (
|
||||
<span className="flex flex-col items-center gap-3 text-white/55">
|
||||
<span className="flex flex-col items-center gap-3 text-gray-500">
|
||||
{uploading ? (
|
||||
<Loader2 className="h-7 w-7 animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-7 w-7" />
|
||||
)}
|
||||
<span className="text-sm">
|
||||
点击上传,或拖入图片
|
||||
</span>
|
||||
<span className="text-sm">点击上传,或拖入图片</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
@ -625,26 +730,26 @@ export function StyleProWorkspace({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="font-semibold text-base">历史记录</h2>
|
||||
<span className="text-xs text-white/40">最近 4 条</span>
|
||||
<div className="rounded-xl bg-gray-50 p-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<span className="subsection-title">历史记录</span>
|
||||
<span className="text-gray-400 text-xs">最近 4 条</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{history.length > 0 ? (
|
||||
history.map((item) => (
|
||||
<button
|
||||
className={cn(
|
||||
"group overflow-hidden rounded-lg border bg-white/[0.03] text-left transition hover:border-emerald-300/70",
|
||||
"overflow-hidden rounded-lg border bg-white text-left transition hover:border-blue-400",
|
||||
currentResult?.id === item.id
|
||||
? "border-emerald-300/80"
|
||||
: "border-white/10"
|
||||
? "border-blue-400 shadow-[0_4px_12px_rgba(59,130,246,0.16)]"
|
||||
: "border-gray-200"
|
||||
)}
|
||||
key={item.id}
|
||||
onClick={() => restoreHistoryItem(item)}
|
||||
type="button"
|
||||
>
|
||||
<div className="aspect-square bg-black">
|
||||
<div className="aspect-square bg-gray-100">
|
||||
<img
|
||||
alt="风格化历史"
|
||||
className="h-full w-full object-cover"
|
||||
@ -652,7 +757,7 @@ export function StyleProWorkspace({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 p-2">
|
||||
<div className="flex items-center gap-1 text-[11px] text-white/50">
|
||||
<div className="flex items-center gap-1 text-[11px] text-gray-400">
|
||||
<Clock3 className="h-3 w-3" />
|
||||
{new Date(item.createdAt).toLocaleTimeString(
|
||||
"zh-CN",
|
||||
@ -662,222 +767,184 @@ export function StyleProWorkspace({
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div className="line-clamp-1 text-xs text-white/80">
|
||||
<div className="line-clamp-1 text-gray-700 text-xs">
|
||||
{item.optionLabels[0] || "风格化结果"}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-2 rounded-lg border border-dashed border-white/12 p-5 text-center text-sm text-white/40">
|
||||
<div className="col-span-2 rounded-lg border border-dashed border-gray-300 bg-white p-5 text-center text-gray-400 text-sm">
|
||||
生成后会在这里保存历史。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</section>
|
||||
|
||||
<section className="flex min-w-0 flex-col overflow-hidden bg-[#0b0c0c]">
|
||||
<div className="border-white/10 border-b p-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-emerald-300" />
|
||||
<h2 className="font-semibold text-lg">提示词按钮组合</h2>
|
||||
<div className="rounded-xl bg-gray-50 p-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<span className="subsection-title">生图结果</span>
|
||||
<p className="mt-1 text-gray-500 text-sm">
|
||||
生成后拖动滑块查看原图与结果图。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
className="rounded-lg bg-blue-600 text-white hover:bg-blue-700"
|
||||
disabled={generating || Boolean(disabledReason)}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
{generating ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{generating ? "正在创建图片" : "执行"}
|
||||
</Button>
|
||||
<Button
|
||||
className="rounded-lg border-gray-200 bg-white text-gray-700 hover:bg-gray-100"
|
||||
disabled={!currentResult}
|
||||
onClick={() => currentResult && onAddToCanvas?.(currentResult)}
|
||||
variant="outline"
|
||||
>
|
||||
添加到画布
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-white/45">
|
||||
完成必选项后执行,后台隐藏提示词会参与最终生成。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
className="rounded-lg border-white/10 bg-white/[0.04] text-white hover:bg-white/[0.08]"
|
||||
onClick={quickFillRequired}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
自动补全必选
|
||||
</Button>
|
||||
<Button
|
||||
className="rounded-lg border-white/10 bg-white/[0.04] text-white hover:bg-white/[0.08]"
|
||||
onClick={() => setSelections({})}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
清空选择
|
||||
</Button>
|
||||
</div>
|
||||
<BeforeAfterSlider
|
||||
afterUrl={currentResult?.resultImageUrl}
|
||||
beforeUrl={currentResult?.sourceImageUrl || sourceImageUrl}
|
||||
generating={generating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="space-y-6 p-5">
|
||||
{configLoading ? (
|
||||
<div className="flex min-h-[180px] items-center justify-center rounded-lg border border-white/10 bg-white/[0.03] text-white/55">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
正在加载配置
|
||||
</div>
|
||||
) : !config?.enabled ? (
|
||||
<div className="rounded-lg border border-amber-400/30 bg-amber-400/10 p-5 text-amber-100">
|
||||
风格化 PRO 暂未启用,请联系管理员开启。
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{enabledCategories.map((category, index) => {
|
||||
const selectedIds = selections[category.id] || [];
|
||||
const complete =
|
||||
selectedIds.length >=
|
||||
Math.max(
|
||||
category.required ? 1 : 0,
|
||||
category.minSelections
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg border border-white/10 bg-white/[0.035] p-4"
|
||||
key={category.id}
|
||||
>
|
||||
<div className="mb-4 flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-semibold",
|
||||
complete
|
||||
? "bg-emerald-300 text-black"
|
||||
: "bg-white/[0.08] text-white/65"
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-white">
|
||||
{category.title}
|
||||
</h3>
|
||||
{category.required ? (
|
||||
<Badge className="border-emerald-400/30 bg-emerald-400/10 text-emerald-200">
|
||||
必选
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
className="border-white/10 text-white/55"
|
||||
variant="outline"
|
||||
>
|
||||
可选
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{category.description ? (
|
||||
<p className="mt-1 text-sm text-white/45">
|
||||
{category.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<span className="rounded-full border border-white/10 px-3 py-1 text-xs text-white/45">
|
||||
已选 {selectedIds.length}/{category.maxSelections}
|
||||
{configLoading ? (
|
||||
<section className="section-card flex min-h-[180px] items-center justify-center p-6 text-gray-500">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
正在加载配置
|
||||
</section>
|
||||
) : !config?.enabled ? (
|
||||
<section className="section-card border-amber-200 bg-amber-50 p-6 text-amber-800">
|
||||
风格化 PRO 暂未启用,请联系管理员开启。
|
||||
</section>
|
||||
) : (
|
||||
enabledCategories.map((category) => {
|
||||
const selectedIds = selections[category.id] || [];
|
||||
return (
|
||||
<section className="section-card p-6" key={category.id}>
|
||||
<div className="rounded-xl bg-gray-50 p-4">
|
||||
<div className="mb-3 flex items-start gap-4">
|
||||
<span className="subsection-title mt-2 w-20">
|
||||
{category.title}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-3 flex flex-wrap items-center gap-2">
|
||||
{category.required ? (
|
||||
<Badge className="border-blue-200 bg-blue-50 text-blue-800">
|
||||
必选
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="border-gray-200 bg-white text-gray-500">
|
||||
可选
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-gray-400 text-xs">
|
||||
已选 {selectedIds.length}/{category.maxSelections}
|
||||
</span>
|
||||
{category.description ? (
|
||||
<span className="text-gray-400 text-xs">
|
||||
{category.description}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{category.options.map((option) => {
|
||||
const selected = selectedIds.includes(option.id);
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"min-h-[96px] rounded-lg border p-4 text-left transition",
|
||||
selected
|
||||
? "border-emerald-300/80 bg-emerald-300/12 shadow-[0_0_0_1px_rgba(110,231,183,0.25)]"
|
||||
: "border-white/10 bg-black/25 hover:border-white/25 hover:bg-white/[0.05]"
|
||||
)}
|
||||
key={option.id}
|
||||
onClick={() => toggleOption(category, option.id)}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<span className="font-semibold text-sm text-white">
|
||||
{option.label}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="rounded-full bg-emerald-300 px-2 py-0.5 text-[11px] font-semibold text-black">
|
||||
已选
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{option.description ? (
|
||||
<p className="mt-2 line-clamp-2 text-xs leading-relaxed text-white/45">
|
||||
{option.description}
|
||||
</p>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{category.options.map((option) => {
|
||||
const selected = selectedIds.includes(option.id);
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"option-btn rounded-full px-4 py-2 font-medium text-sm",
|
||||
selected && "active"
|
||||
)}
|
||||
key={option.id}
|
||||
onClick={() => toggleOption(category, option.id)}
|
||||
title={option.description || option.label}
|
||||
type="button"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
className="custom-input w-full rounded-lg px-4 py-2 text-sm"
|
||||
disabled
|
||||
placeholder={`自定义${category.title}描述...`}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border border-white/10 bg-white/[0.035] p-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="font-semibold text-lg">图像对比</h2>
|
||||
<p className="mt-1 text-sm text-white/45">
|
||||
生成完成后拖动滑块查看前后差异。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
className="rounded-lg bg-emerald-400 text-black hover:bg-emerald-300"
|
||||
disabled={generating || Boolean(disabledReason)}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
{generating ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{generating ? "生成中" : "开始生成"}
|
||||
</Button>
|
||||
<Button
|
||||
className="rounded-lg border-white/10 bg-white/[0.04] text-white hover:bg-white/[0.08]"
|
||||
disabled={!currentResult}
|
||||
onClick={() => currentResult && onAddToCanvas?.(currentResult)}
|
||||
variant="outline"
|
||||
>
|
||||
添加到画布
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{disabledReason ? (
|
||||
<div className="mb-3 rounded-lg border border-white/10 bg-black/35 px-3 py-2 text-sm text-white/45">
|
||||
{disabledReason}
|
||||
</div>
|
||||
) : null}
|
||||
<BeforeAfterSlider
|
||||
afterUrl={currentResult?.resultImageUrl}
|
||||
beforeUrl={currentResult?.sourceImageUrl || sourceImageUrl}
|
||||
generating={generating}
|
||||
/>
|
||||
{selectedOptionLabels.length > 0 ? (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{selectedOptionLabels.map((label) => (
|
||||
<Badge
|
||||
className="border-white/10 bg-white/[0.04] text-white/70"
|
||||
key={label}
|
||||
variant="outline"
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section className="section-card p-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="flex items-center gap-2 font-bold text-gray-900">
|
||||
<Sparkles className="h-5 w-5 text-blue-600" />
|
||||
已选组合
|
||||
</h3>
|
||||
<p className="mt-1 text-gray-500 text-sm">
|
||||
按钮提示词只在服务端参与生成,前台仅展示按钮名称。
|
||||
</p>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
className="rounded-lg border-gray-200 bg-white text-gray-700 hover:bg-gray-100"
|
||||
onClick={() => setSelections({})}
|
||||
variant="outline"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
清空选择
|
||||
</Button>
|
||||
<Button
|
||||
className="rounded-lg bg-blue-600 text-white hover:bg-blue-700"
|
||||
disabled={generating || Boolean(disabledReason)}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
{generating ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
执行
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{selectedOptionLabels.length > 0 ? (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{selectedOptionLabels.map((label) => (
|
||||
<Badge
|
||||
className="border-blue-200 bg-blue-50 text-blue-800"
|
||||
key={label}
|
||||
variant="outline"
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-5 text-center text-gray-400 text-sm">
|
||||
还没有选择按钮。
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user