feat: improve app ui and project metadata
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, ArrowRight, CheckCircle2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import { api } from "@/lib/api/client";
|
||||
import { useToast } from "@/components/Toast";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Field } from "@/components/ui/Field";
|
||||
import { Select } from "@/components/ui/Select";
|
||||
import { TextArea } from "@/components/ui/TextArea";
|
||||
import { TextInput } from "@/components/ui/TextInput";
|
||||
import { api } from "@/lib/api/client";
|
||||
import { buttonClass } from "@/lib/ui/variants";
|
||||
import {
|
||||
GENRES,
|
||||
SELLING_POINT_PRESETS,
|
||||
@@ -69,14 +77,17 @@ export function ProjectWizard() {
|
||||
const isLast = step === WIZARD_STEPS;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl rounded border border-line bg-panel p-8 shadow-paper">
|
||||
<div className="mx-auto max-w-2xl rounded border border-line bg-panel p-6 shadow-paper sm:p-8">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h1 className="font-serif text-2xl text-ink">新建作品</h1>
|
||||
<StepDots current={step} />
|
||||
</div>
|
||||
<p className="mb-4 text-sm text-ink-soft">
|
||||
步骤 {step}/{WIZARD_STEPS} · {STEP_TITLES[step - 1]}
|
||||
</p>
|
||||
<div className="mb-4">
|
||||
<Badge variant="accent">
|
||||
步骤 {step}/{WIZARD_STEPS}
|
||||
</Badge>
|
||||
<p className="mt-2 text-sm text-ink-soft">{STEP_TITLES[step - 1]}</p>
|
||||
</div>
|
||||
|
||||
<div className="min-h-[220px]">
|
||||
{step === 1 && <StepBasics form={form} update={update} />}
|
||||
@@ -93,32 +104,28 @@ export function ProjectWizard() {
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={goBack}
|
||||
disabled={step === 1}
|
||||
className="rounded border border-line px-4 py-2 text-sm text-ink disabled:opacity-40"
|
||||
>
|
||||
← 上一步
|
||||
</button>
|
||||
<Button onClick={goBack} disabled={step === 1} variant="secondary">
|
||||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
||||
上一步
|
||||
</Button>
|
||||
{isLast ? (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
onClick={submit}
|
||||
disabled={submitting || !canSubmit(form)}
|
||||
className="rounded bg-cinnabar px-5 py-2 text-sm text-panel disabled:opacity-40"
|
||||
variant="primary"
|
||||
>
|
||||
{submitting ? "创建中…" : "完成立项 →"}
|
||||
</button>
|
||||
<CheckCircle2 className="h-4 w-4" aria-hidden="true" />
|
||||
{submitting ? "创建中…" : "完成立项"}
|
||||
</Button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
onClick={goNext}
|
||||
disabled={!canAdvance(step, form)}
|
||||
className="rounded bg-cinnabar px-5 py-2 text-sm text-panel disabled:opacity-40"
|
||||
variant="primary"
|
||||
>
|
||||
下一步 →
|
||||
</button>
|
||||
下一步
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,7 +138,7 @@ function StepDots({ current }: { current: number }) {
|
||||
{Array.from({ length: WIZARD_STEPS }, (_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`h-2 w-2 rounded-full ${
|
||||
className={`h-2 w-2 rounded ${
|
||||
i + 1 <= current ? "bg-cinnabar" : "bg-line"
|
||||
}`}
|
||||
/>
|
||||
@@ -145,30 +152,11 @@ interface StepProps {
|
||||
update: (patch: Partial<WizardForm>) => void;
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<label className="mb-4 block">
|
||||
<span className="mb-1 block text-sm text-ink-soft">{label}</span>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
const inputCls =
|
||||
"w-full rounded border border-line bg-bg px-3 py-2 text-ink focus:border-cinnabar focus:outline-none";
|
||||
|
||||
function StepBasics({ form, update }: StepProps) {
|
||||
return (
|
||||
<div>
|
||||
<Field label="书名(必填)">
|
||||
<input
|
||||
className={inputCls}
|
||||
<TextInput
|
||||
value={form.title}
|
||||
onChange={(e) => update({ title: e.target.value })}
|
||||
placeholder="例:逐光而行"
|
||||
@@ -176,8 +164,7 @@ function StepBasics({ form, update }: StepProps) {
|
||||
/>
|
||||
</Field>
|
||||
<Field label="题材">
|
||||
<select
|
||||
className={inputCls}
|
||||
<Select
|
||||
value={form.genre}
|
||||
onChange={(e) => update({ genre: e.target.value })}
|
||||
>
|
||||
@@ -187,7 +174,7 @@ function StepBasics({ form, update }: StepProps) {
|
||||
{g}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
@@ -201,8 +188,8 @@ function StepStory({
|
||||
return (
|
||||
<div>
|
||||
<Field label="一句话故事(logline)">
|
||||
<textarea
|
||||
className={`${inputCls} h-20 resize-none`}
|
||||
<TextArea
|
||||
className="h-20 resize-none"
|
||||
value={form.logline}
|
||||
onChange={(e) => update({ logline: e.target.value })}
|
||||
placeholder="废柴少年觉醒禁忌血脉,在仙门倾轧中逆势封神。"
|
||||
@@ -215,11 +202,10 @@ function StepStory({
|
||||
type="button"
|
||||
key={s}
|
||||
onClick={() => update({ structure: s })}
|
||||
className={`rounded border px-3 py-1.5 text-sm ${
|
||||
form.structure === s
|
||||
? "border-cinnabar bg-[var(--color-cinnabar-wash)] text-cinnabar"
|
||||
: "border-line text-ink"
|
||||
}`}
|
||||
className={buttonClass({
|
||||
variant: form.structure === s ? "outline" : "secondary",
|
||||
size: "sm",
|
||||
})}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
@@ -235,11 +221,12 @@ function StepStory({
|
||||
key={p}
|
||||
aria-pressed={form.sellingPoints.includes(p)}
|
||||
onClick={() => toggleSellingPoint(p)}
|
||||
className={`rounded border px-3 py-1.5 text-sm ${
|
||||
form.sellingPoints.includes(p)
|
||||
? "border-cinnabar bg-[var(--color-cinnabar-wash)] text-cinnabar"
|
||||
: "border-line text-ink"
|
||||
}`}
|
||||
className={buttonClass({
|
||||
variant: form.sellingPoints.includes(p)
|
||||
? "outline"
|
||||
: "secondary",
|
||||
size: "sm",
|
||||
})}
|
||||
>
|
||||
{p}
|
||||
</button>
|
||||
@@ -254,16 +241,15 @@ function StepPremise({ form, update }: StepProps) {
|
||||
return (
|
||||
<div>
|
||||
<Field label="立意(premise)">
|
||||
<textarea
|
||||
className={`${inputCls} h-20 resize-none`}
|
||||
<TextArea
|
||||
className="h-20 resize-none"
|
||||
value={form.premise}
|
||||
onChange={(e) => update({ premise: e.target.value })}
|
||||
placeholder="故事的核心命题与前提。"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="主题(theme)">
|
||||
<input
|
||||
className={inputCls}
|
||||
<TextInput
|
||||
value={form.theme}
|
||||
onChange={(e) => update({ theme: e.target.value })}
|
||||
placeholder="例:抗争与代价"
|
||||
@@ -282,8 +268,8 @@ function StepProtagonist({ form, update }: StepProps) {
|
||||
主角与金手指(M1 暂并入总纲,后续设定库独立建模)。
|
||||
</p>
|
||||
<Field label="主角 / 金手指概要">
|
||||
<textarea
|
||||
className={`${inputCls} h-28 resize-none`}
|
||||
<TextArea
|
||||
className="h-28 resize-none"
|
||||
value={form.protagonist}
|
||||
onChange={(e) => update({ protagonist: e.target.value })}
|
||||
placeholder="主角设定、金手指来源与限制……"
|
||||
@@ -295,7 +281,7 @@ function StepProtagonist({ form, update }: StepProps) {
|
||||
|
||||
function StepWorld() {
|
||||
return (
|
||||
<div className="rounded border border-dashed border-line bg-bg p-6 text-sm text-ink-soft">
|
||||
<div className="rounded border border-dashed border-line bg-bg p-6 text-sm leading-6 text-ink-soft">
|
||||
基础世界观将在「设定库」里建模(后续里程碑)。现在可直接完成立项,进入工作台开始写第一章。
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user