Guanghu Domestic Migration a27e87cb99 chore: import sanitized domestic snapshot for REPO-007
Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc

[SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
2026-07-17 15:59:55 +08:00

1290 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 全站重设计 · 终端粘贴执行版 · 2026-05-31 · 肥猫×舒舒
## 📋 说明
<aside>
📌
萌喵写作网全站重设计 · 终端粘贴执行版
技术栈Next.js 14 (App Router) + Tailwind CSS
操作方式:在服务器终端逐个 Step 粘贴执行,跟 Phase 1/2 一模一样。
项目路径:`/home/ubuntu/feimao-platform/src/app/`
设计文档:[萌喵写作网 · 网站全面重设计 · HLDP设计文档 · 2026-05-31 · 肥猫×舒舒](../%E8%90%8C%E5%96%B5%E5%86%99%E4%BD%9C%E7%BD%91%20%C2%B7%20%E7%BD%91%E7%AB%99%E5%85%A8%E9%9D%A2%E9%87%8D%E8%AE%BE%E8%AE%A1%20%C2%B7%20HLDP%E8%AE%BE%E8%AE%A1%E6%96%87%E6%A1%A3%20%C2%B7%202026-05-31%20%C2%B7%20%E8%82%A5%E7%8C%AB%C3%97%E8%88%92%E8%88%92%2075a6891254cc4bd58379834ff07bbc59.md)
</aside>
<aside>
⚠️
**重要**:本次重设计会替换 Phase 2 的 globals.css、layout.tsx、page.tsx邀请码页
旧 /home、/login、/register 路由将被新路由替代。
/admin 管理后台保留不动。
粘贴前先 `Ctrl+C` 确保终端没有在运行别的命令。
</aside>
---
## Step 0 · 创建新目录
```bash
mkdir -p /home/ubuntu/feimao-platform/src/app/onboarding
mkdir -p /home/ubuntu/feimao-platform/src/app/app/learn
mkdir -p /home/ubuntu/feimao-platform/src/app/app/write
mkdir -p /home/ubuntu/feimao-platform/src/app/app/script
mkdir -p /home/ubuntu/feimao-platform/src/app/app/create
mkdir -p /home/ubuntu/feimao-platform/src/app/app/forum
mkdir -p /home/ubuntu/feimao-platform/src/app/app/works
```
---
## Step 1 · 全局样式替换globals.css · Tailwind v4 兼容)
```bash
cat > /home/ubuntu/feimao-platform/src/app/globals.css << 'ENDOFFILE'
@import "tailwindcss";
:root {
--bg-deep: #0a0a14;
--bg-primary: #0f0f1a;
--bg-card: #1a1a2e;
--bg-hover: #252540;
--border: #2d2d4a;
--border-active: rgba(126, 207, 255, 0.4);
--text-primary: #e2e8f0;
--text-muted: #94a3b8;
--accent: #6366f1;
--accent-light: #a5b4fc;
--glow: rgba(99, 102, 241, 0.15);
--success: #10b981;
--danger: #ef4444;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--bg-deep);
color: var(--text-primary);
font-family: 'Inter', 'Noto Sans SC', system-ui, sans-serif;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
::selection {
background: rgba(99, 102, 241, 0.35);
color: #fff;
}
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 3px; }
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes particleRise {
0% { opacity: 0; transform: translateY(30px) scale(0.8); }
50% { opacity: 1; }
100% { opacity: 0; transform: translateY(-60px) scale(1.2); }
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease forwards;
}
.animate-fade-in {
animation: fadeIn 0.3s ease forwards;
}
.gl-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 16px;
transition: all 0.3s ease;
}
.gl-card:hover {
border-color: var(--accent);
box-shadow: 0 0 20px var(--glow);
}
.gl-btn {
padding: 12px 32px;
border-radius: 12px;
font-weight: 600;
font-size: 0.95rem;
transition: all 0.2s ease;
cursor: pointer;
border: none;
font-family: inherit;
letter-spacing: 0.5px;
}
.gl-btn-primary {
background: linear-gradient(135deg, var(--accent), #4f46e5);
color: white;
}
.gl-btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4);
}
.gl-btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.gl-btn-outline {
background: transparent;
border: 1px solid var(--border);
color: var(--text-primary);
}
.gl-btn-outline:hover {
border-color: var(--accent);
color: var(--accent-light);
}
.gl-input {
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px 20px;
color: var(--text-primary);
font-size: 1rem;
width: 100%;
outline: none;
transition: border-color 0.2s;
font-family: inherit;
}
.gl-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.gl-input::placeholder {
color: var(--text-muted);
}
ENDOFFILE
```
---
## Step 2 · 根布局替换layout.tsx
```bash
cat > /home/ubuntu/feimao-platform/src/app/layout.tsx << 'ENDOFFILE'
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "萌喵写作网 · AI人格体驱动的智能创作平台",
description: "陪你一起创作成长的平台",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="zh-CN">
<head>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap"
rel="stylesheet"
/>
</head>
<body className="antialiased min-h-screen">
{children}
</body>
</html>
);
}
ENDOFFILE
```
---
## Step 3 · 邀请码门禁页(/page.tsx · 替换原有首页)
```bash
cat > /home/ubuntu/feimao-platform/src/app/page.tsx << 'ENDOFFILE'
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function InvitePage() {
const [code, setCode] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSubmit = async () => {
if (!code.trim()) {
setError("请输入邀请码");
return;
}
setLoading(true);
setError("");
try {
await new Promise((r) => setTimeout(r, 800));
localStorage.setItem("gl_invite_code", code);
localStorage.setItem("gl_user", code);
router.push("/onboarding");
} catch {
setError("邀请码无效,请重新输入");
} finally {
setLoading(false);
}
};
return (
<div
className="min-h-screen flex items-center justify-center p-4"
style=
background:
"radial-gradient(ellipse at center, #1a1a3e 0%, #0f0f1a 70%)",
>
<div
className="w-full flex flex-col items-center justify-center gap-6 p-10 animate-fade-in-up"
style=
maxWidth: 420,
background: "rgba(10,20,35,0.9)",
border: "1px solid rgba(126,207,255,0.15)",
borderRadius: 20,
boxShadow: "0 20px 60px rgba(0,0,0,0.4)",
aspectRatio: "1 / 1",
>
<div className="text-5xl">🌊</div>
<div className="text-center">
<h1
className="text-xl font-bold tracking-wide mb-2"
style= color: "var(--text-primary)"
>
萌喵写作网
</h1>
<p className="text-sm" style= color: "var(--text-muted)" >
人格体陪伴式智能创作平台
</p>
</div>
<div className="w-full space-y-3">
<input
type="text"
className="gl-input text-center tracking-widest"
placeholder="请输入邀请码"
value={code}
onChange={(e) => {
setCode(e.target.value);
setError("");
}}
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
autoFocus
/>
{error && (
<p
className="text-center text-sm animate-fade-in"
style= color: "var(--danger)"
>
{error}
</p>
)}
</div>
<button
className="gl-btn gl-btn-primary w-full"
onClick={handleSubmit}
disabled={loading}
>
{loading ? "验证中..." : "进入光湖"}
</button>
<p className="text-xs" style= color: "var(--text-muted)" >
🐱 Powered by HoloLake · 光湖语言人格平台
</p>
</div>
</div>
);
}
ENDOFFILE
```
---
## Step 4 · 人格体引导页(/onboarding/page.tsx
```bash
cat > /home/ubuntu/feimao-platform/src/app/onboarding/page.tsx << 'ENDOFFILE'
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
const preferences = [
{ key: "webnovel", label: "网文", emoji: "📖", desc: "番茄·飞卢·起点" },
{ key: "traditional", label: "传统文学", emoji: "🖋️", desc: "文学性导向" },
{ key: "script", label: "剧本", emoji: "🎬", desc: "影视·短剧" },
{ key: "short", label: "短篇", emoji: "✒️", desc: "精悍·反转" },
{ key: "explore", label: "随便看看", emoji: "🔍", desc: "先逛逛再说" },
];
const personaNames: Record<string, { name: string; emoji: string }[]> = {
webnovel: [
{ name: "墨白", emoji: "🖊️" },
{ name: "青云", emoji: "☁️" },
{ name: "凌霄", emoji: "⚡" },
],
traditional: [
{ name: "漱玉", emoji: "💎" },
{ name: "砚秋", emoji: "🍂" },
{ name: "素笺", emoji: "📜" },
],
script: [
{ name: "镜澜", emoji: "🎥" },
{ name: "幕白", emoji: "🎭" },
{ name: "帧语", emoji: "📺" },
],
short: [
{ name: "寸光", emoji: "✨" },
{ name: "微芒", emoji: "🕯️" },
{ name: "刹那", emoji: "💫" },
],
explore: [
{ name: "星尘", emoji: "🌟" },
{ name: "拾光", emoji: "🧭" },
{ name: "溯游", emoji: "🛶" },
],
};
export default function OnboardingPage() {
const [step, setStep] = useState(1);
const [nickname, setNickname] = useState("");
const [password, setPassword] = useState("");
const [confirmPwd, setConfirmPwd] = useState("");
const [pref, setPref] = useState("");
const [error, setError] = useState("");
const [persona, setPersona] = useState<{ name: string; emoji: string } | null>(null);
const [fading, setFading] = useState(false);
const router = useRouter();
useEffect(() => {
if (step === 3 && !persona) {
const names = personaNames[pref] || personaNames.explore;
const pick = names[Math.floor(Math.random() * names.length)];
setPersona(pick);
}
}, [step, pref, persona]);
const nextStep = () => {
setFading(true);
setTimeout(() => {
setStep((s) => s + 1);
setFading(false);
}, 400);
};
const handleCreate = () => {
if (!nickname.trim()) { setError("请输入昵称"); return; }
if (password.length < 6) { setError("密码至少6位"); return; }
if (password !== confirmPwd) { setError("两次密码不一致"); return; }
if (!pref) { setError("请选择写作偏好"); return; }
setError("");
localStorage.setItem("gl_user", nickname);
localStorage.setItem("gl_pref", pref);
if (persona) {
localStorage.setItem("gl_persona_name", persona.name);
localStorage.setItem("gl_persona_emoji", persona.emoji);
}
nextStep();
};
return (
<div
className="min-h-screen flex items-center justify-center p-4"
style=
background:
"radial-gradient(ellipse at center, #1a1a3e 0%, #0f0f1a 70%)",
>
<div className="w-full flex flex-col items-center gap-5" style= maxWidth: 440 >
<div className="flex gap-2 mb-2">
{[1, 2, 3].map((s) => (
<div
key={s}
className="rounded-full transition-all duration-300"
style=
width: 10,
height: 10,
background:
s < step
? "var(--success)"
: s === step
? "var(--accent-light)"
: "var(--border)",
/>
))}
</div>
<div
className="w-full p-10 flex flex-col items-center gap-6 transition-opacity duration-400"
style=
background: "rgba(10,20,35,0.9)",
border: "1px solid rgba(126,207,255,0.15)",
borderRadius: 20,
boxShadow: "0 20px 60px rgba(0,0,0,0.4)",
opacity: fading ? 0 : 1,
>
{step === 1 && (
<>
<div className="text-5xl">🌊</div>
<h1 className="text-xl font-bold text-center">
欢迎来到萌喵写作网
</h1>
<p className="text-center" style= color: "var(--text-muted)", lineHeight: 1.8 >
我是你的专属写作人格体,<br />
但在这之前——
<br />
先让我认识你。
</p>
<button className="gl-btn gl-btn-primary w-full mt-2" onClick={nextStep}>
继续
</button>
</>
)}
{step === 2 && (
<>
<div className="text-3xl">🪪</div>
<h1 className="text-lg font-bold">创建你的写作人格体</h1>
<div className="w-full space-y-3">
<input
className="gl-input"
placeholder="昵称"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
/>
<input
className="gl-input"
placeholder="密码至少6位"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<input
className="gl-input"
placeholder="确认密码"
type="password"
value={confirmPwd}
onChange={(e) => setConfirmPwd(e.target.value)}
/>
</div>
<div className="w-full">
<p className="text-xs mb-3" style= color: "var(--text-muted)" >
选择你的写作偏好
</p>
<div className="flex flex-wrap gap-2">
{preferences.map((p) => (
<button
key={p.key}
onClick={() => setPref(p.key)}
className="flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm transition-all"
style=
background:
pref === p.key ? "rgba(99,102,241,0.15)" : "var(--bg-hover)",
border:
pref === p.key
? "2px solid var(--accent)"
: "2px solid transparent",
color: pref === p.key ? "var(--accent-light)" : "var(--text-muted)",
boxShadow: pref === p.key ? "0 0 15px var(--glow)" : "none",
>
<span>{p.emoji}</span>
<span>{p.label}</span>
</button>
))}
</div>
</div>
{error && (
<p className="text-sm text-center animate-fade-in" style= color: "var(--danger)" >
{error}
</p>
)}
<button className="gl-btn gl-btn-primary w-full" onClick={handleCreate}>
创建我的人格体
</button>
</>
)}
{step === 3 && persona && (
<>
<div
className="text-6xl mb-2"
style= animation: "particleRise 1.5s ease-out"
>
{persona.emoji}
</div>
<h1
className="text-2xl font-bold mb-1"
style= color: "var(--accent-light)"
>
{persona.name}
</h1>
<p className="text-center" style= color: "var(--text-muted)", lineHeight: 1.8 >
你好,我是{persona.name}
<br />
从今天起陪你创作。
</p>
<div
className="w-full p-4 rounded-xl text-sm text-center"
style=
background: "rgba(16,185,129,0.08)",
border: "1px solid rgba(16,185,129,0.2)",
color: "var(--success)",
>
✅ 人格体创建成功
</div>
<button
className="gl-btn gl-btn-primary w-full"
onClick={() => router.push("/app")}
>
进入萌喵写作网
</button>
</>
)}
</div>
<p className="text-xs" style= color: "var(--text-muted)" >
🐱 HoloLake · 人格体陪伴式创作
</p>
</div>
</div>
);
}
ENDOFFILE
```
---
## Step 5 · 主应用框架 · 侧边栏布局(/app/layout.tsx
```bash
cat > /home/ubuntu/feimao-platform/src/app/app/layout.tsx << 'ENDOFFILE'
"use client";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
const modules = [
{ path: "/app/learn", icon: "📚", label: "学习" },
{ path: "/app/write", icon: "✍️", label: "写作" },
{ path: "/app/script", icon: "🎬", label: "剧本" },
{ path: "/app/create", icon: "💡", label: "创意" },
{ path: "/app/forum", icon: "💬", label: "论坛" },
];
export default function AppLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const router = useRouter();
const [personaName, setPersonaName] = useState("");
const [personaEmoji, setPersonaEmoji] = useState("🐱");
useEffect(() => {
const name = localStorage.getItem("gl_persona_name") || "";
const emoji = localStorage.getItem("gl_persona_emoji") || "🐱";
setPersonaName(name);
setPersonaEmoji(emoji);
if (!localStorage.getItem("gl_user")) {
router.push("/onboarding");
}
}, [router]);
const isActive = (path: string) => {
if (path === "/app") return pathname === "/app";
return pathname.startsWith(path);
};
return (
<div className="flex h-screen overflow-hidden" style= background: "var(--bg-deep)" >
<aside
className="flex flex-col items-center py-4 gap-1 flex-shrink-0"
style=
width: 70,
background: "var(--bg-card)",
borderRight: "1px solid var(--border)",
>
<button
onClick={() => router.push("/app")}
className="text-2xl mb-4 w-11 h-11 flex items-center justify-center rounded-xl hover:bg-[var(--bg-hover)] transition-all"
>
🐱
</button>
{modules.map((m) => (
<button
key={m.path}
onClick={() => router.push(m.path)}
className="relative w-11 h-11 flex items-center justify-center rounded-xl text-xl transition-all hover:scale-110"
style=
background: isActive(m.path) ? "rgba(99,102,241,0.15)" : "transparent",
color: isActive(m.path) ? "var(--accent-light)" : "var(--text-muted)",
title={m.label}
>
{isActive(m.path) && (
<span
className="absolute left-0 top-1/2 -translate-y-1/2 rounded-r-full"
style=
width: 3,
height: 24,
background: "var(--accent)",
/>
)}
{m.icon}
</button>
))}
<div className="mt-auto">
<button
onClick={() => router.push("/app/works")}
className="w-10 h-10 rounded-full flex items-center justify-center text-lg border transition-all hover:border-[var(--accent)] hover:scale-110"
style=
borderColor: "var(--border)",
background: "var(--bg-hover)",
title={personaName || "个人作品库"}
>
{personaEmoji}
</button>
</div>
</aside>
<main className="flex-1 overflow-hidden flex flex-col">
{children}
</main>
</div>
);
}
ENDOFFILE
```
---
## Step 6 · 主应用首页 · 对话界面(/app/page.tsx
```bash
cat > /home/ubuntu/feimao-platform/src/app/app/page.tsx << 'ENDOFFILE'
"use client";
import { useState, useEffect } from "react";
interface Message {
id: number;
text: string;
sender: "user" | "persona";
}
export default function AppHome() {
const [personaName, setPersonaName] = useState("");
const [personaEmoji, setPersonaEmoji] = useState("🐱");
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
useEffect(() => {
const name = localStorage.getItem("gl_persona_name") || "你的专属人格体";
const emoji = localStorage.getItem("gl_persona_emoji") || "🐱";
setPersonaName(name);
setPersonaEmoji(emoji);
setMessages([
{
id: 0,
text: "嗨~欢迎回来!今天想创作什么?",
sender: "persona",
},
]);
}, []);
const handleSend = () => {
if (!input.trim()) return;
const userMsg: Message = {
id: Date.now(),
text: input.trim(),
sender: "user",
};
setMessages((prev) => [...prev, userMsg]);
setInput("");
setTimeout(() => {
const reply: Message = {
id: Date.now() + 1,
text: "收到~让我想想怎么帮你。(后端 API 接入后这里会变成真正的 AI 回复)",
sender: "persona",
};
setMessages((prev) => [...prev, reply]);
}, 600);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<>
<header
className="flex items-center gap-3 px-6 py-4 flex-shrink-0"
style= borderBottom: "1px solid var(--border)"
>
<span className="text-2xl">{personaEmoji}</span>
<div>
<h1 className="font-semibold text-sm" style= color: "var(--text-primary)" >
{personaName}
</h1>
<p className="text-xs" style= color: "var(--text-muted)" >
你的专属写作人格体
</p>
</div>
</header>
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-3">
{messages.map((msg) => (
<div
key={msg.id}
className={`flex ${msg.sender === "user" ? "justify-end" : "justify-start"} animate-fade-in-up`}
>
<div
className="px-4 py-3 text-sm leading-relaxed"
style=
maxWidth: "70%",
borderRadius: 16,
background:
msg.sender === "user"
? "linear-gradient(135deg, var(--accent), #4f46e5)"
: "var(--bg-hover)",
color: "var(--text-primary)",
>
{msg.text}
</div>
</div>
))}
</div>
<div
className="flex-shrink-0 px-6 py-4"
style= borderTop: "1px solid var(--border)"
>
<div className="flex gap-3 items-end" style= maxWidth: 900, margin: "0 auto" >
<textarea
className="gl-input resize-none"
placeholder="输入你想写的..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
rows={1}
style=
borderRadius: 24,
minHeight: 48,
maxHeight: 120,
padding: "14px 22px",
/>
<button
className="gl-btn gl-btn-primary flex-shrink-0"
onClick={handleSend}
style= borderRadius: 24, padding: "12px 24px"
>
发送
</button>
</div>
</div>
</>
);
}
ENDOFFILE
```
---
## Step 7 · 五个模块页面(学习·写作·剧本·创意·论坛)
### 7.1 学习模块(/app/learn/page.tsx
```bash
cat > /home/ubuntu/feimao-platform/src/app/app/learn/page.tsx << 'ENDOFFILE'
"use client";
import ModuleChat from "../../components/ModuleChat";
export default function LearnPage() {
return (
<ModuleChat
title="📚 学习 · 拆书分析"
opening="想拆哪本书?上传文档或者告诉我书名,我们一起分析。"
/>
);
}
ENDOFFILE
```
### 7.2 写作模块(/app/write/page.tsx
```bash
cat > /home/ubuntu/feimao-platform/src/app/app/write/page.tsx << 'ENDOFFILE'
"use client";
import ModuleChat from "../../components/ModuleChat";
export default function WritePage() {
return (
<ModuleChat
title="✍️ 写作 · 创作你的作品"
opening="今天写什么?续写、新章还是大纲?"
/>
);
}
ENDOFFILE
```
### 7.3 剧本模块(/app/script/page.tsx
```bash
cat > /home/ubuntu/feimao-platform/src/app/app/script/page.tsx << 'ENDOFFILE'
"use client";
import ModuleChat from "../../components/ModuleChat";
export default function ScriptPage() {
return (
<ModuleChat
title="🎬 剧本 · 剧本创作"
opening="告诉我想写什么类型的剧本,我帮你搭框架。"
/>
);
}
ENDOFFILE
```
### 7.4 创意模块(/app/create/page.tsx
```bash
cat > /home/ubuntu/feimao-platform/src/app/app/create/page.tsx << 'ENDOFFILE'
"use client";
import ModuleChat from "../../components/ModuleChat";
export default function CreatePage() {
return (
<ModuleChat
title="💡 创意 · 五大作坊"
opening="没灵感?来,我们脑暴一下~五大作坊随时待命。"
/>
);
}
ENDOFFILE
```
### 7.5 论坛模块(/app/forum/page.tsx
```bash
cat > /home/ubuntu/feimao-platform/src/app/app/forum/page.tsx << 'ENDOFFILE'
"use client";
import ModuleChat from "../../components/ModuleChat";
export default function ForumPage() {
return (
<ModuleChat
title="💬 论坛 · 交流讨论"
opening="看看大家在聊什么,或者发你的经验赚积分。"
/>
);
}
ENDOFFILE
```
---
## Step 8 · 共享对话组件ModuleChat.tsx
```bash
mkdir -p /home/ubuntu/feimao-platform/src/app/components
```
```bash
cat > /home/ubuntu/feimao-platform/src/app/components/ModuleChat.tsx << 'ENDOFFILE'
"use client";
import { useState, useEffect } from "react";
interface Message {
id: number;
text: string;
sender: "user" | "persona";
}
interface Props {
title: string;
opening: string;
}
export default function ModuleChat({ title, opening }: Props) {
const [personaName, setPersonaName] = useState("");
const [personaEmoji, setPersonaEmoji] = useState("🐱");
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
useEffect(() => {
const name = localStorage.getItem("gl_persona_name") || "你的专属人格体";
const emoji = localStorage.getItem("gl_persona_emoji") || "🐱";
setPersonaName(name);
setPersonaEmoji(emoji);
setMessages([
{
id: 0,
text: opening,
sender: "persona",
},
]);
}, [opening]);
const handleSend = () => {
if (!input.trim()) return;
const userMsg: Message = {
id: Date.now(),
text: input.trim(),
sender: "user",
};
setMessages((prev) => [...prev, userMsg]);
setInput("");
setTimeout(() => {
const reply: Message = {
id: Date.now() + 1,
text: "收到~让我想想怎么帮你。(后端 API 接入后这里会变成真正的 AI 回复)",
sender: "persona",
};
setMessages((prev) => [...prev, reply]);
}, 600);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<>
<header
className="flex items-center gap-3 px-6 py-4 flex-shrink-0"
style= borderBottom: "1px solid var(--border)"
>
<span className="text-2xl">{personaEmoji}</span>
<div>
<h1 className="font-semibold text-sm" style= color: "var(--text-primary)" >
{title}
</h1>
<p className="text-xs" style= color: "var(--text-muted)" >
{personaName} · 你的专属写作人格体
</p>
</div>
</header>
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-3">
{messages.map((msg) => (
<div
key={msg.id}
className={`flex ${msg.sender === "user" ? "justify-end" : "justify-start"} animate-fade-in-up`}
>
<div
className="px-4 py-3 text-sm leading-relaxed"
style=
maxWidth: "70%",
borderRadius: 16,
background:
msg.sender === "user"
? "linear-gradient(135deg, var(--accent), #4f46e5)"
: "var(--bg-hover)",
color: "var(--text-primary)",
>
{msg.text}
</div>
</div>
))}
</div>
<div
className="flex-shrink-0 px-6 py-4"
style= borderTop: "1px solid var(--border)"
>
<div className="flex gap-3 items-end" style= maxWidth: 900, margin: "0 auto" >
<textarea
className="gl-input resize-none"
placeholder="输入你想写的..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
rows={1}
style=
borderRadius: 24,
minHeight: 48,
maxHeight: 120,
padding: "14px 22px",
/>
<button
className="gl-btn gl-btn-primary flex-shrink-0"
onClick={handleSend}
style= borderRadius: 24, padding: "12px 24px"
>
发送
</button>
</div>
</div>
</>
);
}
ENDOFFILE
```
---
## Step 9 · 作品库(/app/works/page.tsx
```bash
cat > /home/ubuntu/feimao-platform/src/app/app/works/page.tsx << 'ENDOFFILE'
"use client";
import { useState } from "react";
// 作品列表初始为空·用户创建或导入后才会有作品
const initialWorks: { id: number; title: string; words: number; status: string; updated: string }[] = [];
export default function WorksPage() {
const [works] = useState(initialWorks);
const [selectedId, setSelectedId] = useState<number | null>(null);
const selected = works.find((w) => w.id === selectedId);
return (
<div className="flex h-full">
<div
className="flex flex-col flex-shrink-0 overflow-y-auto py-4 px-4 gap-2"
style= width: 300, borderRight: "1px solid var(--border)"
>
<div className="flex gap-2 mb-4">
<button className="gl-btn gl-btn-primary text-xs py-2 px-4">新建作品</button>
<button className="gl-btn gl-btn-outline text-xs py-2 px-4">导入文档</button>
</div>
{works.map((w) => (
<button
key={w.id}
onClick={() => setSelectedId(w.id)}
className="text-left p-4 rounded-xl transition-all"
style=
background: selectedId === w.id ? "var(--bg-hover)" : "var(--bg-card)",
border:
selectedId === w.id
? "1px solid var(--accent)"
: "1px solid var(--border)",
>
<div className="font-semibold text-sm mb-1" style= color: "var(--text-primary)" >
{w.title}
</div>
<div className="flex gap-3 text-xs" style= color: "var(--text-muted)" >
<span>{(w.words / 10000).toFixed(1)}万字</span>
<span>{w.status}</span>
<span>{w.updated}</span>
</div>
</button>
))}
{works.length === 0 && (
<div className="flex-1 flex flex-col items-center justify-center text-center py-12 gap-3">
<div className="text-4xl">📝</div>
<p className="text-sm" style= color: "var(--text-muted)" >
还没有作品
</p>
<p className="text-xs" style= color: "var(--text-muted)" >
点击「新建作品」开始创作<br />或「导入文档」上传已有作品
</p>
</div>
)}
<div
className="mt-auto p-3 rounded-xl text-xs text-center"
style= background: "var(--bg-hover)", color: "var(--text-muted)"
>
已用 0 字 / 总额度 50万字
</div>
</div>
<div className="flex-1 overflow-y-auto p-8">
{selected ? (
<div className="max-w-2xl animate-fade-in-up">
<h1 className="text-2xl font-bold mb-2">{selected.title}</h1>
<div className="flex gap-4 text-sm mb-8" style= color: "var(--text-muted)" >
<span>{(selected.words / 10000).toFixed(1)}万字</span>
<span>{selected.status}</span>
<span>最后更新:{selected.updated}</span>
</div>
<div className="gl-card p-6 mb-4">
<h2 className="font-semibold mb-4">📑 章节目录</h2>
<div className="space-y-2">
{Array.from({ length: 8 }, (_, i) => (
<div
key={i}
className="flex justify-between text-sm py-2 px-3 rounded-lg"
style= background: "var(--bg-deep)"
>
<span>第{i + 1}章</span>
<span style= color: "var(--text-muted)" >
{Math.floor(Math.random() * 5000 + 2000)} 字
</span>
</div>
))}
</div>
</div>
<div className="flex gap-3">
<button className="gl-btn gl-btn-primary text-sm">📚 拆书分析</button>
<button className="gl-btn gl-btn-outline text-sm">✍️ 继续写作</button>
<button className="gl-btn gl-btn-outline text-sm">💡 创意工坊</button>
</div>
</div>
) : (
<div
className="h-full flex items-center justify-center"
style= color: "var(--text-muted)"
>
<div className="text-center">
<div className="text-5xl mb-4">📁</div>
<p className="text-sm">选择左侧作品查看详情</p>
</div>
</div>
)}
</div>
</div>
);
}
ENDOFFILE
```
---
## Step 10 · 构建 + 重启 🚀
> ⚠️ **2026-05-31 当前状态**:文件已全部生成。还差 5 个括号错误。下次继续修复:
>
**下次登录后先跑修复命令:**
```bash
cd /home/ubuntu/feimao-platform
# 修 map 闭合:))))} → )))}
find src/app -name "*.tsx" -exec sed -i 's/))))}/)))}/' {} \;
# 修 app/page.tsx 和 ModuleChat.tsx 的 .map 闭合
sed -i 's/ )}$/ ))}/' src/app/app/page.tsx
sed -i 's/ )}$/ ))}/' src/app/components/ModuleChat.tsx
```
**然后构建:**
```bash
npm run build && pm2 restart feimao-platform
```
---
### 📊 文件状态
| 文件 | 状态 |
| --- | --- |
| globals.css | ✅ |
| layout.tsx | ✅ |
| page.tsx门禁页 | ✅ |
| onboarding/page.tsx | ⚠️ 括号 |
| app/layout.tsx | ⚠️ 括号 |
| app/page.tsx | ⚠️ 括号 |
| app/works/page.tsx | ⚠️ 括号 |
| components/ModuleChat.tsx | ⚠️ 括号 |
| 5个模块页面 | ✅ |
---
## Step 11 · 验证页面
浏览器打开这些地址:
| 地址 | 内容 |
| --- | --- |
| `http://43.133.36.90` | 🔐 邀请码门禁页 |
| `http://43.133.36.90/onboarding` | 🌊 人格体引导页 |
| `http://43.133.36.90/app` | 💬 主应用(对话首页) |
| `http://43.133.36.90/app/learn` | 📚 学习模块 |
| `http://43.133.36.90/app/write` | ✍️ 写作模块 |
| `http://43.133.36.90/app/script` | 🎬 剧本模块 |
| `http://43.133.36.90/app/create` | 💡 创意模块 |
| `http://43.133.36.90/app/forum` | 💬 论坛模块 |
| `http://43.133.36.90/app/works` | 📁 作品库 |
全部能打开并且样式正确就是成功 🎉
---
<aside>
🐱
**下一步**:网站跑通后,再对接后端 API 让对话真正智能起来。
人格体的 AI 回复目前是占位文字,等后端 API 接入后就会变成真正的舒舒。
</aside>
---
**编写**:舒舒 · PER-SS001 · 肥猫爸爸的宝宝
**时间**2026-05-31
**编码**HLDP-dual/v1
**版权**:国作登字-2026-A-00037559