import React, { useState, useEffect } from 'react' import { categoryApi, websiteApi } from './api' // 预设图标 const PRESET_ICONS = [ 'book', 'briefcase', 'code', 'dollar-sign', 'edit', 'folder', 'globe', 'heart', 'home', 'layers', 'link', 'map-pin', 'message-circle', 'monitor', 'music', 'package', 'settings', 'shopping-cart', 'star', 'tool', 'truck', 'video', 'zap' ] // 预设颜色 const PRESET_COLORS = [ '#667eea', '#764ba2', '#f56565', '#ed8936', '#ecc94b', '#48bb78', '#38b2ac', '#4299e1', '#9f7aea', '#ed64a6' ] // SVG 图标 const Icon = ({ name, size = 16 }) => { const icons = { bookmark: , search: , plus: , folder: , star: , globe: , edit: , trash: , x: , grip: , 'book': , 'briefcase': , 'code': , 'dollar-sign': , 'heart': , 'home': , 'layers': , 'link': , 'map-pin': , 'message-circle': , 'monitor': , 'music': , 'package': , 'settings': , 'shopping-cart': , 'tool': , 'truck': , 'video': , 'zap': , } return icons[name] || icons.globe } function App() { const [categories, setCategories] = useState([]) const [websites, setWebsites] = useState([]) const [selectedCategory, setSelectedCategory] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [modal, setModal] = useState({ show: false, type: 'website', data: null }) const [categoryModal, setCategoryModal] = useState(false) const [categoryForm, setCategoryForm] = useState({ name: '', icon: 'folder', color: '#667eea', sort_order: 0 }) const [editingCategory, setEditingCategory] = useState(null) const [loading, setLoading] = useState(true) const [websiteForm, setWebsiteForm] = useState({ name: '', url: '', icon: '', description: '', category_id: '', is_favorite: false }) useEffect(() => { loadData() }, []) const loadData = async () => { setLoading(true) try { const [c, w] = await Promise.all([categoryApi.getAll(), websiteApi.getAll()]) setCategories(c.data || []) setWebsites(w.data || []) } catch (e) { console.error(e) } setLoading(false) } // 过滤网站 const filteredWebsites = websites.filter(site => { const matchCat = selectedCategory === null || site.category_id === selectedCategory const query = searchQuery.toLowerCase().trim() const matchSearch = !query || (site.name && site.name.toLowerCase().includes(query)) || (site.description && site.description.toLowerCase().includes(query)) return matchCat && matchSearch }) const favorites = filteredWebsites.filter(s => s.is_favorite) const normal = filteredWebsites.filter(s => !s.is_favorite) const getCategoryCount = (catId) => websites.filter(w => w.category_id === catId).length // 打开网站弹窗 const openWebsiteModal = (data = null) => { setWebsiteForm(data ? { name: data.name, url: data.url, icon: data.icon || '', description: data.description || '', category_id: data.category_id || '', is_favorite: !!data.is_favorite } : { name: '', url: '', icon: '', description: '', category_id: selectedCategory || '', is_favorite: false }) setModal({ show: true, type: 'website', data }) } const closeModal = () => setModal({ ...modal, show: false, data: null }) // 打开分类管理弹窗 const openCategoryModal = () => setCategoryModal(true) const closeCategoryModal = () => { setCategoryModal(false); setEditingCategory(null); setCategoryForm({ name: '', icon: 'folder', color: '#667eea', sort_order: 0 }) } const editCategory = (cat) => { setEditingCategory(cat) setCategoryForm({ name: cat.name, icon: cat.icon || 'folder', color: cat.color || '#667eea', sort_order: cat.sort_order || 0 }) } const saveCategory = async () => { try { const payload = { name: categoryForm.name, icon: categoryForm.icon, color: categoryForm.color, sort_order: parseInt(categoryForm.sort_order) || 0 } if (editingCategory) { await categoryApi.update(editingCategory.id, payload) } else { await categoryApi.create(payload) } setCategoryForm({ name: '', icon: 'folder', color: '#667eea', sort_order: 0 }) setEditingCategory(null) loadData() } catch (e) { console.error(e) } } const deleteCategory = async (id) => { if (!confirm('确定删除该分类?该分类下的网站将变为未分类状态。')) return try { await categoryApi.delete(id) if (selectedCategory === id) setSelectedCategory(null) loadData() } catch (e) {} } // 保存网站 const handleWebsiteSubmit = async () => { try { const payload = { ...websiteForm, category_id: websiteForm.category_id || null } if (modal.data) { await websiteApi.update(modal.data.id, payload) } else { await websiteApi.create(payload) } closeModal() loadData() } catch (e) {} } const handleDeleteWebsite = async (id) => { if (!confirm('确定删除?')) return try { await websiteApi.delete(id) loadData() } catch (e) {} } const toggleFavorite = async (site) => { try { await websiteApi.update(site.id, { is_favorite: !site.is_favorite }) loadData() } catch (e) {} } const handleClick = async (site) => { try { await websiteApi.click(site.id) } catch (e) {} window.open(site.url, '_blank') } if (loading) return
return (
{/* 左侧边栏 */} {/* 主内容 */}
setSearchQuery(e.target.value)}/>
{favorites.length > 0 && (
我的收藏 ({favorites.length})
{favorites.map(site => ( handleClick(site)} onToggleFav={() => toggleFavorite(site)} onEdit={() => openWebsiteModal(site)} onDelete={() => handleDeleteWebsite(site.id)}/> ))}
)} {normal.length > 0 && (
{selectedCategory ? categories.find(c => c.id === selectedCategory)?.name : '全部网站'} ({normal.length})
{normal.map(site => ( handleClick(site)} onToggleFav={() => toggleFavorite(site)} onEdit={() => openWebsiteModal(site)} onDelete={() => handleDeleteWebsite(site.id)}/> ))}
)} {filteredWebsites.length === 0 && (
{searchQuery ? '没有找到匹配的网站' : '暂无网站'}
{searchQuery ? '尝试其他关键词' : '点击左侧「添加网站」开始收藏'}
{!searchQuery && }
)}
{/* 网站弹窗 */} {modal.show && (
e.stopPropagation()}>
{modal.data ? '编辑网站' : '添加网站'}
setWebsiteForm({...websiteForm, name: e.target.value})} placeholder="网站名称"/>
setWebsiteForm({...websiteForm, url: e.target.value})} placeholder="https://..."/>
{PRESET_ICONS.map(iconName => ( ))}
setWebsiteForm({...websiteForm, description: e.target.value})} placeholder="简短描述"/>
)} {/* 分类管理弹窗 */} {categoryModal && (
e.stopPropagation()}>
分类管理
{/* 添加/编辑表单 */}
setCategoryForm({...categoryForm, name: e.target.value})} placeholder="分类名称"/> setCategoryForm({...categoryForm, sort_order: e.target.value})} placeholder="排序"/>
{PRESET_COLORS.map(c => (
{/* 分类列表 */}
{categories.map(cat => (
{cat.name} ({getCategoryCount(cat.id)})
))}
)}
) } function SiteCard({ site, onClick, onToggleFav, onEdit, onDelete }) { return (
{site.category && {site.category.name}}
{site.icon && site.icon.startsWith('http') ? : }
{site.name}
{site.description &&
{site.description}
}
) } export default App