删除多个完成报告文件,优化项目结构以提升可维护性。
This commit is contained in:
14
components/view/config/config-loader.tsx
Normal file
14
components/view/config/config-loader.tsx
Normal 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
|
||||
}
|
||||
94
components/view/layout/bottom-nav.tsx
Normal file
94
components/view/layout/bottom-nav.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
43
components/view/layout/layout-wrapper.tsx
Normal file
43
components/view/layout/layout-wrapper.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
60
components/view/ui/button.tsx
Normal file
60
components/view/ui/button.tsx
Normal 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 }
|
||||
Reference in New Issue
Block a user