@@ -0,0 +1,639 @@
import React , { useState , useEffect } from 'react'
import {
Search , Plus , FolderOpen , Star , ExternalLink ,
Trash2 , Edit3 , X , Check , Globe , Bookmark ,
MessageCircle , Code , Briefcase , Gamepad2 , BookOpen , Folder
} from 'lucide-react'
import { categoryApi , websiteApi } from './api'
// 图标映射
const iconMap = {
'message-circle' : MessageCircle ,
'code' : Code ,
'briefcase' : Briefcase ,
'gamepad-2' : Gamepad2 ,
'book-open' : BookOpen ,
'folder' : Folder ,
'globe' : Globe ,
'star' : Star ,
}
function App ( ) {
const [ categories , setCategories ] = useState ( [ ] )
const [ websites , setWebsites ] = useState ( [ ] )
const [ selectedCategory , setSelectedCategory ] = useState ( null )
const [ searchQuery , setSearchQuery ] = useState ( '' )
const [ showAddModal , setShowAddModal ] = useState ( false )
const [ showCategoryModal , setShowCategoryModal ] = useState ( false )
const [ editingWebsite , setEditingWebsite ] = useState ( null )
const [ editingCategory , setEditingCategory ] = useState ( null )
const [ loading , setLoading ] = useState ( true )
// 表单状态
const [ websiteForm , setWebsiteForm ] = useState ( {
name : '' ,
url : '' ,
icon : '' ,
description : '' ,
category _id : '' ,
is _favorite : false ,
} )
const [ categoryForm , setCategoryForm ] = useState ( {
name : '' ,
icon : 'folder' ,
color : '#6366f1' ,
} )
// 加载数据
useEffect ( ( ) => {
loadData ( )
} , [ ] )
const loadData = async ( ) => {
setLoading ( true )
try {
const [ catRes , webRes ] = await Promise . all ( [
categoryApi . getAll ( ) ,
websiteApi . getAll ( ) ,
] )
setCategories ( catRes . data )
setWebsites ( webRes . data )
} catch ( error ) {
console . error ( '加载数据失败:' , error )
}
setLoading ( false )
}
// 过滤网站
const filteredWebsites = websites . filter ( site => {
const matchCategory = selectedCategory === null || site . category _id === selectedCategory
const matchSearch = searchQuery === '' ||
site . name . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) ||
site . description . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) )
return matchCategory && matchSearch
} )
// 收藏网站单独显示
const favoriteWebsites = filteredWebsites . filter ( s => s . is _favorite )
const normalWebsites = filteredWebsites . filter ( s => ! s . is _favorite )
// 添加网站
const handleAddWebsite = async ( ) => {
try {
await websiteApi . create ( {
... websiteForm ,
category _id : websiteForm . category _id || null ,
} )
setShowAddModal ( false )
resetWebsiteForm ( )
loadData ( )
} catch ( error ) {
console . error ( '添加网站失败:' , error )
}
}
// 更新网站
const handleUpdateWebsite = async ( ) => {
try {
await websiteApi . update ( editingWebsite . id , {
... websiteForm ,
category _id : websiteForm . category _id || null ,
} )
setEditingWebsite ( null )
setShowAddModal ( false )
resetWebsiteForm ( )
loadData ( )
} catch ( error ) {
console . error ( '更新网站失败:' , error )
}
}
// 删除网站
const handleDeleteWebsite = async ( id ) => {
if ( ! confirm ( '确定要删除这个网站吗?' ) ) return
try {
await websiteApi . delete ( id )
loadData ( )
} catch ( error ) {
console . error ( '删除网站失败:' , error )
}
}
// 切换收藏
const toggleFavorite = async ( site ) => {
try {
await websiteApi . update ( site . id , { is _favorite : ! site . is _favorite } )
loadData ( )
} catch ( error ) {
console . error ( '切换收藏失败:' , error )
}
}
// 点击网站
const handleClickWebsite = async ( site ) => {
try {
await websiteApi . click ( site . id )
window . open ( site . url , '_blank' , 'noopener,noreferrer' )
} catch ( error ) {
console . error ( '点击失败:' , error )
window . open ( site . url , '_blank' , 'noopener,noreferrer' )
}
}
// 重置表单
const resetWebsiteForm = ( ) => {
setWebsiteForm ( {
name : '' ,
url : '' ,
icon : '' ,
description : '' ,
category _id : '' ,
is _favorite : false ,
} )
}
// 添加分类
const handleAddCategory = async ( ) => {
try {
await categoryApi . create ( categoryForm )
setShowCategoryModal ( false )
setEditingCategory ( null )
setCategoryForm ( { name : '' , icon : 'folder' , color : '#6366f1' } )
loadData ( )
} catch ( error ) {
console . error ( '添加分类失败:' , error )
}
}
// 更新分类
const handleUpdateCategory = async ( ) => {
try {
await categoryApi . update ( editingCategory . id , categoryForm )
setEditingCategory ( null )
setShowCategoryModal ( false )
setCategoryForm ( { name : '' , icon : 'folder' , color : '#6366f1' } )
loadData ( )
} catch ( error ) {
console . error ( '更新分类失败:' , error )
}
}
// 删除分类
const handleDeleteCategory = async ( id ) => {
if ( ! confirm ( '确定要删除这个分类吗?分类下的网站将变为未分类状态。' ) ) return
try {
await categoryApi . delete ( id )
if ( selectedCategory === id ) setSelectedCategory ( null )
loadData ( )
} catch ( error ) {
console . error ( '删除分类失败:' , error )
}
}
// 编辑网站
const openEditWebsite = ( site ) => {
setEditingWebsite ( site )
setWebsiteForm ( {
name : site . name ,
url : site . url ,
icon : site . icon || '' ,
description : site . description || '' ,
category _id : site . category _id || '' ,
is _favorite : site . is _favorite ,
} )
setShowAddModal ( true )
}
// 编辑分类
const openEditCategory = ( cat ) => {
setEditingCategory ( cat )
setCategoryForm ( {
name : cat . name ,
icon : cat . icon ,
color : cat . color ,
} )
setShowCategoryModal ( true )
}
// 获取网站图标
const getWebsiteIcon = ( site ) => {
if ( site . icon && site . icon . startsWith ( 'http' ) ) {
return < img src = { site . icon } alt = "" className = "w-8 h-8 rounded-lg object-cover" / >
}
return < Globe className = "w-8 h-8 text-indigo-400" / >
}
// 渲染图标选择器
const renderIconPicker = ( ) => {
const icons = [ 'folder' , 'globe' , 'star' , 'message-circle' , 'code' , 'briefcase' , 'gamepad-2' , 'book-open' ]
return (
< div className = "flex gap-2 flex-wrap" >
{ icons . map ( icon => {
const IconComponent = iconMap [ icon ]
return (
< button
key = { icon }
type = "button"
onClick = { ( ) => setCategoryForm ( { ... categoryForm , icon } ) }
className = { ` p-3 rounded-lg border-2 transition-all ${
categoryForm . icon === icon
? 'border-indigo-500 bg-indigo-500/20'
: 'border-[#2a2f3a] hover:border-[#3a3f4a]'
} ` }
>
< IconComponent className = "w-5 h-5" / >
< / button >
)
} ) }
< / div >
)
}
if ( loading ) {
return (
< div className = "min-h-screen flex items-center justify-center" >
< div className = "animate-spin rounded-full h-12 w-12 border-4 border-indigo-500 border-t-transparent" > < / div >
< / div >
)
}
return (
< div className = "min-h-screen bg-[#0d0f12]" >
{ /* 头部 */ }
< header className = "sticky top-0 z-40 glass border-b border-[#2a2f3a]" >
< div className = "max-w-7xl mx-auto px-4 py-4" >
< div className = "flex items-center justify-between gap-4" >
< div className = "flex items-center gap-3" >
< div className = "w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 flex items-center justify-center" >
< Bookmark className = "w-5 h-5 text-white" / >
< / div >
< h1 className = "text-xl font-bold gradient-text hidden sm:block" > 导航收藏夹 < / h1 >
< / div >
{ /* 搜索 */ }
< div className = "flex-1 max-w-md relative" >
< Search className = "absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[#64748b]" / >
< input
type = "text"
placeholder = "搜索网站..."
value = { searchQuery }
onChange = { ( e ) => setSearchQuery ( e . target . value ) }
className = "w-full pl-10 pr-4 py-2.5 bg-[#1a1d24] border border-[#2a2f3a] rounded-xl text-[#f1f5f9] placeholder-[#64748b] transition-all"
/ >
< / div >
{ /* 操作按钮 */ }
< div className = "flex items-center gap-2" >
< button
onClick = { ( ) => { setEditingCategory ( null ) ; setShowCategoryModal ( true ) ; setCategoryForm ( { name : '' , icon : 'folder' , color : '#6366f1' } ) } }
className = "p-2.5 bg-[#1a1d24] border border-[#2a2f3a] rounded-xl hover:border-[#3a3f4a] transition-all btn-press"
title = "添加分类"
>
< FolderOpen className = "w-5 h-5 text-[#94a3b8]" / >
< / button >
< button
onClick = { ( ) => { setEditingWebsite ( null ) ; setShowAddModal ( true ) ; resetWebsiteForm ( ) } }
className = "flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-xl font-medium hover:opacity-90 transition-all btn-press"
>
< Plus className = "w-5 h-5" / >
< span className = "hidden sm:inline" > 添加网站 < / span >
< / button >
< / div >
< / div >
< / div >
< / header >
< main className = "max-w-7xl mx-auto px-4 py-6" >
{ /* 分类标签 */ }
< div className = "flex items-center gap-2 mb-8 overflow-x-auto pb-2" >
< button
onClick = { ( ) => setSelectedCategory ( null ) }
className = { ` px-4 py-2 rounded-xl font-medium whitespace-nowrap transition-all ${
selectedCategory === null
? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white'
: 'bg-[#1a1d24] text-[#94a3b8] hover:text-[#f1f5f9] border border-[#2a2f3a]'
} ` }
>
全部
< / button >
{ categories . map ( cat => {
const IconComponent = iconMap [ cat . icon ] || Folder
return (
< div
key = { cat . id }
className = "flex items-center gap-1 group"
>
< button
onClick = { ( ) => setSelectedCategory ( cat . id ) }
className = { ` flex items-center gap-2 px-4 py-2 rounded-xl font-medium whitespace-nowrap transition-all ${
selectedCategory === cat . id
? 'text-white'
: 'bg-[#1a1d24] text-[#94a3b8] hover:text-[#f1f5f9] border border-[#2a2f3a]'
} ` }
style = { selectedCategory === cat . id ? { background : ` linear-gradient(135deg, ${ cat . color } 99, ${ cat . color } 66) ` } : { } }
>
< IconComponent className = "w-4 h-4" style = { { color : cat . color } } / >
{ cat . name }
< / button >
< div className = "hidden group-hover:flex items-center gap-1" >
< button
onClick = { ( ) => openEditCategory ( cat ) }
className = "p-1 hover:bg-[#2a2f3a] rounded"
>
< Edit3 className = "w-3 h-3 text-[#64748b]" / >
< / button >
< button
onClick = { ( ) => handleDeleteCategory ( cat . id ) }
className = "p-1 hover:bg-red-500/20 rounded"
>
< Trash2 className = "w-3 h-3 text-red-400" / >
< / button >
< / div >
< / div >
)
} ) }
< / div >
{ /* 收藏网站 */ }
{ favoriteWebsites . length > 0 && (
< section className = "mb-8" >
< h2 className = "flex items-center gap-2 text-lg font-semibold text-[#f1f5f9] mb-4" >
< Star className = "w-5 h-5 text-yellow-500 fill-yellow-500" / >
我的收藏
< / h2 >
< div className = "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4" >
{ favoriteWebsites . map ( site => (
< WebsiteCard
key = { site . id }
site = { site }
onClick = { ( ) => handleClickWebsite ( site ) }
onToggleFavorite = { ( ) => toggleFavorite ( site ) }
onEdit = { ( ) => openEditWebsite ( site ) }
onDelete = { ( ) => handleDeleteWebsite ( site . id ) }
getIcon = { getWebsiteIcon }
/ >
) ) }
< / div >
< / section >
) }
{ /* 普通网站 */ }
{ normalWebsites . length > 0 && (
< section >
< h2 className = "text-lg font-semibold text-[#f1f5f9] mb-4" >
{ selectedCategory
? categories . find ( c => c . id === selectedCategory ) ? . name
: '全部网站' }
< span className = "ml-2 text-[#64748b] font-normal" > ( { normalWebsites . length } ) < / span >
< / h2 >
< div className = "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4" >
{ normalWebsites . map ( site => (
< WebsiteCard
key = { site . id }
site = { site }
onClick = { ( ) => handleClickWebsite ( site ) }
onToggleFavorite = { ( ) => toggleFavorite ( site ) }
onEdit = { ( ) => openEditWebsite ( site ) }
onDelete = { ( ) => handleDeleteWebsite ( site . id ) }
getIcon = { getWebsiteIcon }
/ >
) ) }
< / div >
< / section >
) }
{ /* 空状态 */ }
{ filteredWebsites . length === 0 && (
< div className = "text-center py-20" >
< div className = "w-20 h-20 mx-auto mb-4 rounded-2xl bg-[#1a1d24] flex items-center justify-center" >
< Globe className = "w-10 h-10 text-[#64748b]" / >
< / div >
< h3 className = "text-xl font-semibold text-[#f1f5f9] mb-2" > 暂无网站 < / h3 >
< p className = "text-[#64748b] mb-6" > 点击上方按钮添加你的第一个收藏网站 < / p >
< button
onClick = { ( ) => { setEditingWebsite ( null ) ; setShowAddModal ( true ) ; resetWebsiteForm ( ) } }
className = "inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-xl font-medium hover:opacity-90 transition-all"
>
< Plus className = "w-5 h-5" / >
添加网站
< / button >
< / div >
) }
< / main >
{ /* 添加/编辑网站弹窗 */ }
{ showAddModal && (
< Modal onClose = { ( ) => { setShowAddModal ( false ) ; setEditingWebsite ( null ) ; resetWebsiteForm ( ) } } >
< h2 className = "text-xl font-bold text-[#f1f5f9] mb-6" >
{ editingWebsite ? '编辑网站' : '添加网站' }
< / h2 >
< form onSubmit = { ( e ) => { e . preventDefault ( ) ; editingWebsite ? handleUpdateWebsite ( ) : handleAddWebsite ( ) } } className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-[#94a3b8] mb-2" > 网站名称 * < / label >
< input
type = "text"
required
value = { websiteForm . name }
onChange = { ( e ) => setWebsiteForm ( { ... websiteForm , name : e . target . value } ) }
className = "w-full px-4 py-2.5 bg-[#15181f] border border-[#2a2f3a] rounded-xl text-[#f1f5f9]"
placeholder = "例如:GitHub"
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-[#94a3b8] mb-2" > 网站链接 * < / label >
< input
type = "url"
required
value = { websiteForm . url }
onChange = { ( e ) => setWebsiteForm ( { ... websiteForm , url : e . target . value } ) }
className = "w-full px-4 py-2.5 bg-[#15181f] border border-[#2a2f3a] rounded-xl text-[#f1f5f9]"
placeholder = "https://github.com"
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-[#94a3b8] mb-2" > 图标链接 ( 可选 ) < / label >
< input
type = "url"
value = { websiteForm . icon }
onChange = { ( e ) => setWebsiteForm ( { ... websiteForm , icon : e . target . value } ) }
className = "w-full px-4 py-2.5 bg-[#15181f] border border-[#2a2f3a] rounded-xl text-[#f1f5f9]"
placeholder = "https://example.com/favicon.ico"
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-[#94a3b8] mb-2" > 描述 < / label >
< input
type = "text"
value = { websiteForm . description }
onChange = { ( e ) => setWebsiteForm ( { ... websiteForm , description : e . target . value } ) }
className = "w-full px-4 py-2.5 bg-[#15181f] border border-[#2a2f3a] rounded-xl text-[#f1f5f9]"
placeholder = "简短描述(可选)"
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-[#94a3b8] mb-2" > 分类 < / label >
< select
value = { websiteForm . category _id }
onChange = { ( e ) => setWebsiteForm ( { ... websiteForm , category _id : e . target . value } ) }
className = "w-full px-4 py-2.5 bg-[#15181f] border border-[#2a2f3a] rounded-xl text-[#f1f5f9]"
>
< option value = "" > 未分类 < / option >
{ categories . map ( cat => (
< option key = { cat . id } value = { cat . id } > { cat . name } < / option >
) ) }
< / select >
< / div >
< div className = "flex items-center gap-3" >
< input
type = "checkbox"
id = "favorite"
checked = { websiteForm . is _favorite }
onChange = { ( e ) => setWebsiteForm ( { ... websiteForm , is _favorite : e . target . checked } ) }
className = "w-5 h-5 rounded border-[#2a2f3a] bg-[#15181f] text-indigo-500 focus:ring-indigo-500"
/ >
< label htmlFor = "favorite" className = "text-[#94a3b8]" > 添加到收藏 < / label >
< / div >
< div className = "flex gap-3 pt-4" >
< button
type = "button"
onClick = { ( ) => { setShowAddModal ( false ) ; setEditingWebsite ( null ) ; resetWebsiteForm ( ) } }
className = "flex-1 px-4 py-2.5 bg-[#1a1d24] border border-[#2a2f3a] rounded-xl text-[#94a3b8] hover:text-[#f1f5f9] transition-all"
>
取消
< / button >
< button
type = "submit"
className = "flex-1 px-4 py-2.5 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-xl font-medium hover:opacity-90 transition-all"
>
{ editingWebsite ? '保存' : '添加' }
< / button >
< / div >
< / form >
< / Modal >
) }
{ /* 添加/编辑分类弹窗 */ }
{ showCategoryModal && (
< Modal onClose = { ( ) => { setShowCategoryModal ( false ) ; setEditingCategory ( null ) } } >
< h2 className = "text-xl font-bold text-[#f1f5f9] mb-6" >
{ editingCategory ? '编辑分类' : '添加分类' }
< / h2 >
< form onSubmit = { ( e ) => { e . preventDefault ( ) ; editingCategory ? handleUpdateCategory ( ) : handleAddCategory ( ) } } className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-[#94a3b8] mb-2" > 分类名称 * < / label >
< input
type = "text"
required
value = { categoryForm . name }
onChange = { ( e ) => setCategoryForm ( { ... categoryForm , name : e . target . value } ) }
className = "w-full px-4 py-2.5 bg-[#15181f] border border-[#2a2f3a] rounded-xl text-[#f1f5f9]"
placeholder = "例如:社交媒体"
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-[#94a3b8] mb-2" > 图标 < / label >
{ renderIconPicker ( ) }
< / div >
< div >
< label className = "block text-sm font-medium text-[#94a3b8] mb-2" > 颜色 < / label >
< input
type = "color"
value = { categoryForm . color }
onChange = { ( e ) => setCategoryForm ( { ... categoryForm , color : e . target . value } ) }
className = "w-full h-12 bg-[#15181f] border border-[#2a2f3a] rounded-xl cursor-pointer"
/ >
< / div >
< div className = "flex gap-3 pt-4" >
< button
type = "button"
onClick = { ( ) => { setShowCategoryModal ( false ) ; setEditingCategory ( null ) } }
className = "flex-1 px-4 py-2.5 bg-[#1a1d24] border border-[#2a2f3a] rounded-xl text-[#94a3b8] hover:text-[#f1f5f9] transition-all"
>
取消
< / button >
< button
type = "submit"
className = "flex-1 px-4 py-2.5 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-xl font-medium hover:opacity-90 transition-all"
>
{ editingCategory ? '保存' : '添加' }
< / button >
< / div >
< / form >
< / Modal >
) }
< / div >
)
}
// 网站卡片组件
function WebsiteCard ( { site , onClick , onToggleFavorite , onEdit , onDelete , getIcon } ) {
return (
< div className = "group relative bg-[#1a1d24] border border-[#2a2f3a] rounded-2xl p-4 card-hover cursor-pointer" onClick = { onClick } >
{ /* 操作按钮 */ }
< div className = "absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-10" >
< button
onClick = { ( e ) => { e . stopPropagation ( ) ; onToggleFavorite ( ) } }
className = { ` p-1.5 rounded-lg transition-colors ${ site . is _favorite ? 'bg-yellow-500/20 text-yellow-500' : 'bg-[#2a2f3a] text-[#64748b] hover:text-yellow-500' } ` }
>
< Star className = { ` w-3.5 h-3.5 ${ site . is _favorite ? 'fill-yellow-500' : '' } ` } / >
< / button >
< button
onClick = { ( e ) => { e . stopPropagation ( ) ; onEdit ( ) } }
className = "p-1.5 bg-[#2a2f3a] rounded-lg text-[#64748b] hover:text-indigo-400 transition-colors"
>
< Edit3 className = "w-3.5 h-3.5" / >
< / button >
< button
onClick = { ( e ) => { e . stopPropagation ( ) ; onDelete ( ) } }
className = "p-1.5 bg-[#2a2f3a] rounded-lg text-[#64748b] hover:text-red-400 transition-colors"
>
< Trash2 className = "w-3.5 h-3.5" / >
< / button >
< / div >
{ /* 图标 */ }
< div className = "mb-3" >
{ getIcon ( site ) }
< / div >
{ /* 信息 */ }
< h3 className = "font-semibold text-[#f1f5f9] truncate mb-1" > { site . name } < / h3 >
{ site . description && (
< p className = "text-sm text-[#64748b] truncate" > { site . description } < / p >
) }
{ site . category && (
< span
className = "inline-block mt-2 px-2 py-0.5 rounded-md text-xs font-medium"
style = { {
backgroundColor : ` ${ site . category . color } 20 ` ,
color : site . category . color
} }
>
{ site . category . name }
< / span >
) }
{ /* 外部链接图标 */ }
< div className = "absolute bottom-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity" >
< ExternalLink className = "w-4 h-4 text-[#64748b]" / >
< / div >
< / div >
)
}
// 弹窗组件
function Modal ( { children , onClose } ) {
return (
< div className = "fixed inset-0 z-50 flex items-center justify-center p-4 modal-backdrop animate-fade-in" onClick = { onClose } >
< div
className = "w-full max-w-md bg-[#1a1d24] border border-[#2a2f3a] rounded-2xl p-6 animate-scale-in"
onClick = { ( e ) => e . stopPropagation ( ) }
>
{ children }
< / div >
< / div >
)
}
export default App