删除多个完成报告文件,优化项目结构以提升可维护性。

This commit is contained in:
乘风
2026-02-03 15:59:37 +08:00
parent d4ca9573f5
commit a2443c097c
119 changed files with 2119 additions and 8537 deletions

22
components/README.md Normal file
View File

@@ -0,0 +1,22 @@
# 组件目录说明
## 结构约定
- **移动端C 端)**`view/` — 供 `app/view/*` 使用
- `view/layout/`layout-wrapper、bottom-nav底部导航/view 前缀)
- `view/config/`config-loader
- `view/ui/`:移动端用通用 UI与 admin 各一份,按需复制)
- **管理端**`admin/` — 供 `app/admin/*` 使用
- `admin/ui/`:管理端用通用 UI与 view 各一份)
- `admin/modules/`:管理端业务组件(如 user-detail-modal 等)
- **通用组件**:若 view 与 admin 都用到(如 Button、Card、Dialog`view/ui/``admin/ui/` 各保留一份,便于两端独立样式或行为。
- **当前仍挂在根目录的组件**(如 `search-modal.tsx``chapter-content.tsx``modules/*`)主要被 `app/view/*` 引用,后续可逐步迁入 `view/` 并改为 `@/components/view/...`
## 路由约定
- 根路径 `/` → 重定向到 `/view`(移动端首页)
- 移动端:`/view``/view/chapters``/view/read/[id]``/view/match``/view/my``/view/about``/view/login`
- 管理端:`/admin``/admin/*` 不变

View File

@@ -0,0 +1,60 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -95,7 +95,7 @@ export function BookCover() {
</div>
{/* CTA按钮 - iOS风格 */}
<Link href="/chapters" className="block touch-feedback">
<Link href="/view/chapters" className="block touch-feedback">
<button className="btn-ios w-full flex items-center justify-center gap-2 glow">
<BookOpen className="w-5 h-5" />
<span></span>

View File

@@ -33,19 +33,19 @@ export function BottomNav() {
// 在文档页面、管理后台、阅读页面和关于页面不显示底部导航(必须在所有 hooks 之后)
if (
pathname.startsWith("/documentation") ||
pathname.startsWith("/admin") ||
pathname.startsWith("/read") ||
pathname.startsWith("/about")
pathname?.startsWith("/view/documentation") ||
pathname?.startsWith("/admin") ||
pathname?.startsWith("/view/read") ||
pathname?.startsWith("/view/about")
) {
return null
}
const navItems = [
{ href: "/", icon: Home, label: "首页" },
{ href: "/chapters", icon: List, label: "目录" },
...(matchEnabled ? [{ href: "/match", icon: Users, label: "找伙伴", isCenter: true }] : []),
{ href: "/my", icon: User, label: "我的" },
{ href: "/view", icon: Home, label: "首页" },
{ href: "/view/chapters", icon: List, label: "目录" },
...(matchEnabled ? [{ href: "/view/match", icon: Users, label: "找伙伴", isCenter: true }] : []),
{ href: "/view/my", icon: User, label: "我的" },
]
return (

View File

@@ -41,7 +41,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
const getShareLink = () => {
const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''
const referralCode = user?.referralCode || ''
const shareUrl = `${baseUrl}/read/${section.id}${referralCode ? `?ref=${referralCode}` : ''}`
const shareUrl = `${baseUrl}/view/read/${section.id}${referralCode ? `?ref=${referralCode}` : ''}`
return shareUrl
}
@@ -140,7 +140,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
<header className="sticky top-0 z-40 bg-black/80 backdrop-blur-xl border-b border-white/5">
<div className="max-w-2xl mx-auto px-4 py-3 flex items-center justify-between">
<button
onClick={() => router.push("/chapters")}
onClick={() => router.push("/view/chapters")}
className="w-9 h-9 rounded-full bg-[#1c1c1e] flex items-center justify-center active:bg-[#2c2c2e]"
>
<ChevronLeft className="w-5 h-5 text-gray-400" />
@@ -199,7 +199,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
<div className="flex items-center gap-3">
{prevSection ? (
<button
onClick={() => router.push(`/read/${prevSection.id}`)}
onClick={() => router.push(`/view/read/${prevSection.id}`)}
className="flex-1 max-w-[48%] p-3 rounded-xl bg-[#1c1c1e] border border-white/5 text-left hover:bg-[#2c2c2e] transition-colors"
>
<p className="text-[10px] text-gray-500 mb-0.5"></p>
@@ -211,7 +211,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
{nextSection ? (
<button
onClick={() => router.push(`/read/${nextSection.id}`)}
onClick={() => router.push(`/view/read/${nextSection.id}`)}
className="flex-1 max-w-[48%] p-3 rounded-xl bg-gradient-to-r from-[#00CED1]/10 to-[#20B2AA]/10 border border-[#00CED1]/20 text-left hover:from-[#00CED1]/20 hover:to-[#20B2AA]/20 transition-colors"
>
<p className="text-[10px] text-[#00CED1] mb-0.5"></p>
@@ -390,7 +390,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
</button>
<button
onClick={() => router.push('/my/referral')}
onClick={() => router.push('/view/my/referral')}
className="flex flex-col items-center gap-2 p-3 rounded-xl bg-white/5 hover:bg-white/10 transition-colors"
>
<div className="w-12 h-12 rounded-full bg-[#FFD700]/20 flex items-center justify-center">

View File

@@ -21,7 +21,7 @@ export function ChaptersList({ parts, specialSections }: ChaptersListProps) {
{/* Special sections - Preface */}
{specialSections?.preface && (
<div className="space-y-3">
<Link href={`/read/preface`} className="block group">
<Link href={`/view/read/preface`} className="block group">
<div className="bg-[#0f2137]/60 backdrop-blur-md rounded-xl p-4 border border-transparent hover:border-[#38bdac]/30 transition-all flex items-center justify-between">
<div className="flex items-center gap-3">
<Unlock className="w-4 h-4 text-[#38bdac]" />
@@ -79,7 +79,7 @@ export function ChaptersList({ parts, specialSections }: ChaptersListProps) {
{chapter.sections.map((section) => (
<Link
key={section.id}
href={`/read/${section.id}`}
href={`/view/read/${section.id}`}
className="block px-5 py-4 hover:bg-[#0f2137]/40 transition-colors group"
>
<div className="flex items-center justify-between">
@@ -117,7 +117,7 @@ export function ChaptersList({ parts, specialSections }: ChaptersListProps) {
{/* Special sections - Epilogue */}
{specialSections?.epilogue && (
<div className="mt-8 space-y-3">
<Link href={`/read/epilogue`} className="block group">
<Link href={`/view/read/epilogue`} className="block group">
<div className="bg-[#0f2137]/60 backdrop-blur-md rounded-xl p-4 border border-transparent hover:border-[#38bdac]/30 transition-all flex items-center justify-between">
<div className="flex items-center gap-3">
<Unlock className="w-4 h-4 text-[#38bdac]" />

View File

@@ -23,7 +23,7 @@ export function Footer() {
{/* 链接 */}
<div className="flex items-center justify-center gap-6 text-xs text-[var(--app-text-tertiary)] mb-4">
<a href="/about" className="hover:text-white transition-colors"></a>
<a href="/view/about" className="hover:text-white transition-colors"></a>
<span className="w-1 h-1 rounded-full bg-[var(--app-separator)]" />
<a href="#" className="hover:text-white transition-colors"></a>
<span className="w-1 h-1 rounded-full bg-[var(--app-separator)]" />

View File

@@ -11,13 +11,13 @@ export function BottomNav() {
if (pathname.startsWith("/admin")) return null
const navItems = [
{ href: "/", icon: Home, label: "首页", id: "home" },
{ href: "/match", icon: Users, label: "找伙伴", id: "match" },
{ href: "/my", icon: User, label: "我的", id: "my" },
{ href: "/view", icon: Home, label: "首页", id: "home" },
{ href: "/view/match", icon: Users, label: "找伙伴", id: "match" },
{ href: "/view/my", icon: User, label: "我的", id: "my" },
]
const isActive = (href: string) => {
if (href === "/") return pathname === "/"
if (href === "/view") return pathname === "/view"
return pathname.startsWith(href)
}

View File

@@ -149,7 +149,7 @@ export function AuthModal({ isOpen, onClose, defaultTab = "login" }: AuthModalPr
<div className="text-right">
<Link
href="/login/forgot"
href="/view/login/forgot"
onClick={onClose}
className="text-sm text-[#00CED1] hover:underline"
>

View File

@@ -71,7 +71,7 @@ export function SearchModal({ open, onOpenChange }: SearchModalProps) {
const handleResultClick = (result: SearchResult) => {
onOpenChange(false)
router.push(`/read/${result.id}`)
router.push(`/view/read/${result.id}`)
}
const handleKeywordClick = (keyword: string) => {

View File

@@ -60,7 +60,7 @@ export function TableOfContents({ parts }: TableOfContentsProps) {
{/* 附加内容 - 序言和尾声 */}
<div className="grid grid-cols-2 gap-3">
<Link href="/chapters?section=preface" className="block touch-feedback">
<Link href="/view/chapters?section=preface" className="block touch-feedback">
<div className="glass-card p-4 h-full">
<div className="flex items-center gap-2 mb-2">
<BookOpen className="w-4 h-4 text-[var(--ios-blue)]" />
@@ -72,7 +72,7 @@ export function TableOfContents({ parts }: TableOfContentsProps) {
</div>
</Link>
<Link href="/chapters?section=epilogue" className="block touch-feedback">
<Link href="/view/chapters?section=epilogue" className="block touch-feedback">
<div className="glass-card p-4 h-full">
<div className="flex items-center gap-2 mb-2">
<FileText className="w-4 h-4 text-[var(--ios-purple)]" />

View File

@@ -56,7 +56,7 @@ export function UserMenu() {
{/* Menu items */}
<div className="py-2">
<Link
href="/my/purchases"
href="/view/my/purchases"
className="flex items-center gap-3 px-4 py-3 text-gray-300 hover:bg-gray-800/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
@@ -64,7 +64,7 @@ export function UserMenu() {
<span></span>
</Link>
<Link
href="/my/referral"
href="/view/my/referral"
className="flex items-center gap-3 px-4 py-3 text-gray-300 hover:bg-gray-800/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>

View File

@@ -0,0 +1,14 @@
"use client"
import { useEffect } from "react"
import { useStore } from "@/lib/store"
export function ConfigLoader() {
const { fetchSettings } = useStore()
useEffect(() => {
fetchSettings()
}, [fetchSettings])
return null
}

View File

@@ -0,0 +1,94 @@
"use client"
import { useState, useEffect } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { Home, List, User, Users } from "lucide-react"
export function BottomNav() {
const pathname = usePathname()
const [matchEnabled, setMatchEnabled] = useState(false)
const [configLoaded, setConfigLoaded] = useState(false)
useEffect(() => {
const loadConfig = async () => {
try {
const res = await fetch('/api/db/config')
const data = await res.json()
if (data.features) {
setMatchEnabled(data.features.matchEnabled === true)
}
} catch (e) {
setMatchEnabled(false)
} finally {
setConfigLoaded(true)
}
}
loadConfig()
}, [])
if (
pathname?.startsWith("/view/documentation") ||
pathname?.startsWith("/admin") ||
pathname?.startsWith("/view/read") ||
pathname?.startsWith("/view/about")
) {
return null
}
const navItems = [
{ href: "/view", icon: Home, label: "首页" },
{ href: "/view/chapters", icon: List, label: "目录" },
...(matchEnabled ? [{ href: "/view/match", icon: Users, label: "找伙伴", isCenter: true }] : []),
{ href: "/view/my", icon: User, label: "我的" },
]
return (
<>
<nav className="fixed bottom-0 left-0 right-0 z-40 bg-[#1c1c1e]/95 backdrop-blur-xl border-t border-white/5 safe-bottom">
<div className="flex items-center justify-around py-2 max-w-lg mx-auto">
{navItems.map((item, index) => {
const isActive = pathname === item.href
const Icon = item.icon
if (item.isCenter) {
return (
<Link key={index} href={item.href} className="flex flex-col items-center py-2 px-6 -mt-4">
<div
className={`w-14 h-14 rounded-full flex items-center justify-center shadow-lg transition-all bg-gradient-to-br from-[#00CED1] to-[#20B2AA] shadow-[#00CED1]/30`}
>
<Icon className="w-7 h-7 text-white" />
</div>
<span className={`text-xs mt-1 ${isActive ? "text-[#00CED1] font-medium" : "text-gray-500"}`}>
{item.label}
</span>
</Link>
)
}
return (
<Link
key={index}
href={item.href}
className="flex flex-col items-center py-2 px-4 touch-feedback transition-all duration-200"
>
<div
className={`w-6 h-6 flex items-center justify-center mb-1 transition-colors ${
isActive ? "text-[#00CED1]" : "text-gray-500"
}`}
>
<Icon className="w-5 h-5" strokeWidth={isActive ? 2 : 1.5} />
</div>
<span
className={`text-xs transition-colors ${isActive ? "text-[#00CED1] font-medium" : "text-gray-500"}`}
>
{item.label}
</span>
</Link>
)
})}
</div>
</nav>
</>
)
}

View File

@@ -0,0 +1,43 @@
"use client"
import { usePathname } from "next/navigation"
import { useEffect, useState } from "react"
import { BottomNav } from "./bottom-nav"
import { ConfigLoader } from "../config/config-loader"
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
const [mounted, setMounted] = useState(false)
const isAdmin = pathname?.startsWith("/admin")
const isView = pathname?.startsWith("/view") || pathname === "/"
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return (
<div className="mx-auto max-w-[430px] min-h-screen bg-black shadow-2xl relative font-sans antialiased">
<ConfigLoader />
{children}
</div>
)
}
if (isAdmin) {
return (
<div className="min-h-screen bg-gray-100 text-gray-900 font-sans">
<ConfigLoader />
{children}
</div>
)
}
return (
<div className="mx-auto max-w-[430px] min-h-screen bg-black shadow-2xl relative font-sans antialiased">
<ConfigLoader />
{children}
<BottomNav />
</div>
)
}

View File

@@ -0,0 +1,60 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }