From d088cb4adf7153d32652dc68930deaa5263877fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=BE=99?= Date: Mon, 6 Apr 2026 17:32:00 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加独立分类管理弹窗 - 支持添加、编辑、删除、排序分类 - 10种预设颜色供选择 - 23种预设图标供网站选择 --- frontend/src/App.jsx | 339 +++++++++++++++++++++++++------------------ 1 file changed, 201 insertions(+), 138 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 160ffdc..fe905bf 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,19 @@ 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 = { @@ -9,11 +22,30 @@ const Icon = ({ name, size = 16 }) => { plus: , folder: , star: , - globe: , + globe: , edit: , trash: , x: , - settings: , + 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 } @@ -24,12 +56,11 @@ function App() { 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 [showCategoryManager, setShowCategoryManager] = useState(false) - const [form, setForm] = useState({ - name: '', url: '', icon: '', description: '', category_id: '', is_favorite: false, - color: '#667eea', icon_name: 'folder', sort_order: 0 - }) + const [websiteForm, setWebsiteForm] = useState({ name: '', url: '', icon: '', description: '', category_id: '', is_favorite: false }) useEffect(() => { loadData() }, []) @@ -53,61 +84,84 @@ function App() { 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 openModal = (type, data = null) => { - if (type === 'website') { - setForm(data ? { ...data, category_id: data.category_id || '', is_favorite: !!data.is_favorite } : { name: '', url: '', icon: '', description: '', category_id: '', is_favorite: false, color: '#667eea', icon_name: 'folder', sort_order: 0 }) - } else { - setForm(data ? { - name: data.name, - icon_name: data.icon || 'folder', - color: data.color || '#667eea', - sort_order: data.sort_order || 0 - } : { name: '', icon_name: 'folder', color: '#667eea', sort_order: 0 }) - } - setModal({ show: true, type, data }) + // 打开网站弹窗 + 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 handleSubmit = async () => { + // 打开分类管理弹窗 + 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 { - if (modal.type === 'website') { - const payload = { ...form, category_id: form.category_id || null } - modal.data ? await websiteApi.update(modal.data.id, payload) : await websiteApi.create(payload) - } else { - const payload = { - name: form.name, - icon: form.icon_name, - color: form.color, - sort_order: parseInt(form.sort_order) || 0 - } - modal.data ? await categoryApi.update(modal.data.id, payload) : await categoryApi.create(payload) + const payload = { + name: categoryForm.name, + icon: categoryForm.icon, + color: categoryForm.color, + sort_order: parseInt(categoryForm.sort_order) || 0 } - closeModal() + 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 handleDelete = async (type, id) => { - if (!confirm('确定删除?')) return + const deleteCategory = async (id) => { + if (!confirm('确定删除该分类?该分类下的网站将变为未分类状态。')) return try { - type === 'website' ? await websiteApi.delete(id) : await categoryApi.delete(id) - if (type === 'category' && selectedCategory === id) setSelectedCategory(null) + 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 }) @@ -115,7 +169,6 @@ function App() { } catch (e) {} } - // 点击网站 const handleClick = async (site) => { try { await websiteApi.click(site.id) } catch (e) {} window.open(site.url, '_blank') @@ -150,7 +203,7 @@ function App() { {cat.name} {getCategoryCount(cat.id)} - @@ -158,8 +211,10 @@ function App() {
- - +
@@ -167,18 +222,11 @@ function App() { {/* 主内容 */}
- {/* 搜索 */}
- setSearchQuery(e.target.value)} - /> + setSearchQuery(e.target.value)}/>
- {/* 收藏 */} {favorites.length > 0 && (
@@ -186,13 +234,12 @@ function App() {
{favorites.map(site => ( - handleClick(site)} onToggleFav={() => toggleFavorite(site)} onEdit={() => openModal('website', site)} onDelete={() => handleDelete('website', site.id)}/> + handleClick(site)} onToggleFav={() => toggleFavorite(site)} onEdit={() => openWebsiteModal(site)} onDelete={() => handleDeleteWebsite(site.id)}/> ))}
)} - {/* 普通网站 */} {normal.length > 0 && (
@@ -203,92 +250,122 @@ function App() {
{normal.map(site => ( - handleClick(site)} onToggleFav={() => toggleFavorite(site)} onEdit={() => openModal('website', site)} onDelete={() => handleDelete('website', site.id)}/> + handleClick(site)} onToggleFav={() => toggleFavorite(site)} onEdit={() => openWebsiteModal(site)} onDelete={() => handleDeleteWebsite(site.id)}/> ))}
)} - {/* 空状态 */} {filteredWebsites.length === 0 && (
{searchQuery ? '没有找到匹配的网站' : '暂无网站'}
-
{searchQuery ? '尝试其他关键词' : '点击左侧「添加网站」开始收藏你的第一个链接'}
- {!searchQuery && } +
{searchQuery ? '尝试其他关键词' : '点击左侧「添加网站」开始收藏'}
+ {!searchQuery && }
)}
- {/* 弹窗 */} + {/* 网站弹窗 */} {modal.show && (
e.stopPropagation()}>
-
{modal.data ? '编辑' : '添加'}{modal.type === 'website' ? '网站' : '分类'}
+
{modal.data ? '编辑网站' : '添加网站'}
- {modal.type === 'website' ? ( - <> -
- - setForm({...form, name: e.target.value})} placeholder="网站名称"/> -
-
- - setForm({...form, url: e.target.value})} placeholder="https://..."/> -
-
- - setForm({...form, description: e.target.value})} placeholder="简短描述"/> -
-
- - -
-
- -
- - ) : ( - <> -
- - setForm({...form, name: e.target.value})} placeholder="分类名称"/> -
-
- - setForm({...form, sort_order: e.target.value})} placeholder="0"/> -
-
- - setForm({...form, color: e.target.value})}/> -
- {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)}) +
+
+ + +
+
+ ))}
@@ -297,37 +374,23 @@ function App() { ) } -// 网站卡片 function SiteCard({ site, onClick, onToggleFav, onEdit, onDelete }) { - const handleDelete = (e) => { - e.stopPropagation() - onDelete() - } - const handleEdit = (e) => { - e.stopPropagation() - onEdit() - } - const handleFav = (e) => { - e.stopPropagation() - onToggleFav() - } - return (
{site.category && {site.category.name}}
- {site.icon && site.icon.startsWith('http') ? : } + {site.icon && site.icon.startsWith('http') ? : }
{site.name}
{site.description &&
{site.description}
}
- - -