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

@@ -3,7 +3,8 @@
@tailwind utilities;
/* 纸感设计 tokenUX_SPEC §2.1 */
:root {
:root,
[data-theme="paper"] {
--color-bg: #f5f1e8;
--color-panel: #fbf8f1;
--color-ink: #2b2620;
@@ -15,6 +16,32 @@
--color-overdue: #c8893a;
--color-pass: #5a6b4f;
--color-info: #4a5a6b;
--color-conflict-mark: #b5543a26;
--color-conflict-mark-strong: #b5543a8c;
--shadow-paper: #2b26200f;
color-scheme: light;
}
[data-theme="night"] {
--color-bg: #181614;
--color-panel: #221f1b;
--color-ink: #efe6d7;
--color-ink-soft: #b9ab96;
--color-line: #3c352c;
--color-cinnabar: #e07866;
--color-cinnabar-wash: #e0786620;
--color-conflict: #ef8a72;
--color-overdue: #d8a65d;
--color-pass: #95b47d;
--color-info: #8ca8ca;
--color-conflict-mark: #ef8a7230;
--color-conflict-mark-strong: #ef8a7290;
--shadow-paper: #00000045;
color-scheme: dark;
}
html {
background: var(--color-bg);
}
body {
@@ -22,6 +49,11 @@ body {
color: var(--color-ink);
}
::selection {
background: var(--color-cinnabar-wash);
color: var(--color-ink);
}
/* 流式打字机光标:朱砂闪烁;尊重 prefers-reduced-motionUX §10。 */
.typewriter-cursor {
animation: typewriter-blink 1s step-end infinite;
@@ -42,7 +74,7 @@ body {
/* 终稿正文冲突原文持续高亮朱砂淡底点锚点定位时闪烁一下UX §8.3)。 */
.draft-mark {
background-color: #b5543a26; /* --color-conflict @ ~15% */
background-color: var(--color-conflict-mark);
border-radius: 2px;
}
.draft-mark-flash {
@@ -52,10 +84,10 @@ body {
@keyframes draft-mark-flash {
0%,
100% {
background-color: #b5543a26;
background-color: var(--color-conflict-mark);
}
30% {
background-color: #b5543a8c; /* --color-conflict @ ~55% */
background-color: var(--color-conflict-mark-strong);
}
}

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import "./globals.css";
import { CommandPaletteMount } from "@/components/command/CommandPaletteMount";
import { ThemeScript } from "@/components/ThemeScript";
import { ToastProvider } from "@/components/Toast";
export const metadata: Metadata = {
@@ -19,6 +20,7 @@ export default function RootLayout({
return (
<html lang="zh-CN" suppressHydrationWarning>
<body className="font-sans antialiased" suppressHydrationWarning>
<ThemeScript />
<ToastProvider>
{children}
<CommandPaletteMount />

View File

@@ -1,9 +1,13 @@
import Link from "next/link";
import { BookOpen, Plus } from "lucide-react";
import { AppShell } from "@/components/AppShell";
import { ProjectCard } from "@/components/ProjectCard";
import { ProjectLibrary } from "@/components/projects/ProjectLibrary";
import { EmptyState } from "@/components/ui/EmptyState";
import { PageHeader } from "@/components/ui/PageHeader";
import { fetchProjects } from "@/lib/api/server";
import type { ProjectResponse } from "@/lib/api/types";
import { buttonClass } from "@/lib/ui/variants";
// 作品库Dashboard 入口UX §6.1。Server Component 读取。
export default async function DashboardPage() {
@@ -18,63 +22,44 @@ export default async function DashboardPage() {
return (
<AppShell>
<div className="mx-auto max-w-5xl px-8 py-10">
<div className="mb-8 flex items-center justify-between">
<h1 className="font-serif text-3xl text-ink"></h1>
<Link
href="/projects/new"
className="rounded bg-cinnabar px-4 py-2 text-sm text-panel hover:opacity-90"
>
</Link>
</div>
<div className="mx-auto max-w-5xl px-6 py-10 sm:px-8">
<PageHeader
title="我的作品"
description="从一个灵感进入正文、设定、审稿与验收闭环。"
actions={
<Link
href="/projects/new"
className={buttonClass({ variant: "primary" })}
>
<Plus className="h-4 w-4" aria-hidden="true" />
</Link>
}
/>
{loadError ? (
<p className="rounded border border-conflict bg-panel p-6 text-conflict">
API
</p>
) : projects.length === 0 ? (
<EmptyState />
<EmptyState
icon={BookOpen}
title="还没有作品"
description="从一句灵感开始,后续的设定库、大纲、写章和审稿都会围绕这本书展开。"
action={
<Link
href="/projects/new"
className={buttonClass({ variant: "primary" })}
>
<Plus className="h-4 w-4" aria-hidden="true" />
</Link>
}
/>
) : (
<ul className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
{projects.map((p) => (
<li key={p.id}>
<ProjectCard project={p} />
</li>
))}
<li>
<NewProjectCard />
</li>
</ul>
<ProjectLibrary projects={projects} />
)}
</div>
</AppShell>
);
}
function EmptyState() {
return (
<div className="flex flex-col items-center gap-4 rounded border border-line bg-panel py-20 text-center">
<p className="font-serif text-2xl text-ink"></p>
<p className="text-ink-soft"></p>
<Link
href="/projects/new"
className="rounded bg-cinnabar px-5 py-2 text-sm text-panel hover:opacity-90"
>
</Link>
</div>
);
}
function NewProjectCard() {
return (
<Link
href="/projects/new"
className="flex h-full min-h-[160px] flex-col items-center justify-center rounded border border-dashed border-line bg-panel text-center hover:border-cinnabar"
>
<span className="font-serif text-xl text-cinnabar"> </span>
<span className="mt-2 text-sm text-ink-soft"></span>
</Link>
);
}

View File

@@ -1,5 +1,6 @@
import { AppShell } from "@/components/AppShell";
import { ProvidersSettings } from "@/components/settings/ProvidersSettings";
import { PageHeader } from "@/components/ui/PageHeader";
import { fetchKimiOauthStatus, fetchProviders } from "@/lib/api/server";
import type { ProvidersResponse } from "@/lib/api/types";
@@ -27,7 +28,11 @@ export default async function ProvidersSettingsPage() {
return (
<AppShell title="设置 模型与提供商">
<div className="mx-auto max-w-3xl px-8 py-10">
<div className="mx-auto max-w-6xl px-4 py-8 sm:px-8 sm:py-10">
<PageHeader
title="模型与提供商"
description="配置写手、分析、轻量三类能力档位的路由,并管理 API Key 与 Kimi Code OAuth。"
/>
{loadError ? (
<p className="rounded border border-conflict bg-panel p-6 text-conflict">
API