feat(web): 世界观预览补「仅预览」说明与按卡复制

This commit is contained in:
Yaojia Wang
2026-06-29 16:52:43 +02:00
parent ffe9132f7d
commit f8cabc92e6

View File

@@ -1,14 +1,18 @@
"use client"; "use client";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { Sparkles } from "lucide-react"; import { Copy, Sparkles } from "lucide-react";
import { useToast } from "@/components/Toast";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/Button";
import { Field } from "@/components/ui/Field"; import { Field } from "@/components/ui/Field";
import { SectionHeader } from "@/components/ui/SectionHeader"; import { SectionHeader } from "@/components/ui/SectionHeader";
import { StatusNote } from "@/components/ui/StatusNote";
import { TextArea } from "@/components/ui/TextArea"; import { TextArea } from "@/components/ui/TextArea";
import { worldEntityRules } from "@/lib/generation/cards"; import { worldEntityRules } from "@/lib/generation/cards";
import type { WorldEntityCardView } from "@/lib/api/types";
import { useWorldGen } from "@/lib/generation/useWorldGen"; import { useWorldGen } from "@/lib/generation/useWorldGen";
import { cardClass } from "@/lib/ui/variants";
interface WorldGeneratorProps { interface WorldGeneratorProps {
projectId: string; projectId: string;
@@ -16,14 +20,34 @@ interface WorldGeneratorProps {
// 世界观设计器UX §6.5):一句话需求 → 预览实体卡type/name/rules // 世界观设计器UX §6.5):一句话需求 → 预览实体卡type/name/rules
// 本期世界观入库端点未建(仅预览);预览结果供作者参考/复制。 // 本期世界观入库端点未建(仅预览);预览结果供作者参考/复制。
// 复制文案:名称 + 硬规则正文(供作者粘贴到「规则」或「设定库」)。
function entityClipboardText(entity: WorldEntityCardView): string {
const rules = worldEntityRules(entity);
const body = rules.length > 0 ? rules.join("\n") : "(无显式硬规则)";
return `${entity.name}${entity.type}\n${body}`;
}
export function WorldGenerator({ projectId }: WorldGeneratorProps) { export function WorldGenerator({ projectId }: WorldGeneratorProps) {
const gen = useWorldGen(); const gen = useWorldGen();
const toast = useToast();
const [brief, setBrief] = useState(""); const [brief, setBrief] = useState("");
const onGenerate = useCallback(async () => { const onGenerate = useCallback(async () => {
await gen.generate(projectId, brief); await gen.generate(projectId, brief);
}, [gen, projectId, brief]); }, [gen, projectId, brief]);
const onCopy = useCallback(
async (entity: WorldEntityCardView): Promise<void> => {
try {
await navigator.clipboard.writeText(entityClipboardText(entity));
toast("已复制到剪贴板。", "success");
} catch {
toast("复制失败,请手动选择文本。", "error");
}
},
[toast],
);
const generating = gen.status === "generating"; const generating = gen.status === "generating";
return ( return (
@@ -49,11 +73,15 @@ export function WorldGenerator({ projectId }: WorldGeneratorProps) {
</div> </div>
{gen.status === "done" && gen.entities.length > 0 ? ( {gen.status === "done" && gen.entities.length > 0 ? (
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2"> <div className="flex flex-col gap-3">
{gen.entities.map((entity, i) => ( <StatusNote variant="info">
</StatusNote>
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
{gen.entities.map((entity, i) => (
<article <article
key={`${entity.name}-${i}`} key={`${entity.name}-${i}`}
className="rounded border border-line bg-panel p-3 text-sm" className={cardClass("p-3 text-sm")}
> >
<header className="mb-2 flex items-center gap-2"> <header className="mb-2 flex items-center gap-2">
<span className="font-serif text-base text-ink"> <span className="font-serif text-base text-ink">
@@ -62,6 +90,17 @@ export function WorldGenerator({ projectId }: WorldGeneratorProps) {
<span className="rounded bg-bg px-2 py-0.5 text-xs text-ink-soft"> <span className="rounded bg-bg px-2 py-0.5 text-xs text-ink-soft">
{entity.type} {entity.type}
</span> </span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => void onCopy(entity)}
className="ml-auto"
aria-label={`复制 ${entity.name}`}
>
<Copy className="h-4 w-4" aria-hidden="true" />
</Button>
</header> </header>
{worldEntityRules(entity).length > 0 ? ( {worldEntityRules(entity).length > 0 ? (
<ul className="flex list-disc flex-col gap-1 pl-5 text-ink-soft"> <ul className="flex list-disc flex-col gap-1 pl-5 text-ink-soft">
@@ -73,7 +112,8 @@ export function WorldGenerator({ projectId }: WorldGeneratorProps) {
<p className="text-ink-soft"></p> <p className="text-ink-soft"></p>
)} )}
</article> </article>
))} ))}
</div>
</div> </div>
) : null} ) : null}
</section> </section>