feat: improve app ui and project metadata

This commit is contained in:
Yaojia Wang
2026-06-28 07:31:20 +02:00
parent 3bd464d400
commit 90a66437d7
86 changed files with 4892 additions and 1108 deletions

View File

@@ -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>
);