fix: 修复分类管理和搜索功能
- 添加分类管理功能(添加、编辑、删除、排序) - 修复搜索功能,支持名称和描述搜索 - 侧边栏添加管理分类按钮 - 分类支持sort_order排序 - 修复空状态显示
This commit is contained in:
parent
27c725a8d4
commit
84d9098049
@ -13,6 +13,7 @@ const Icon = ({ name, size = 16 }) => {
|
||||
edit: <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>,
|
||||
trash: <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>,
|
||||
x: <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>,
|
||||
settings: <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0 1.82.33l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>,
|
||||
}
|
||||
return icons[name] || icons.globe
|
||||
}
|
||||
@ -24,7 +25,11 @@ function App() {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [modal, setModal] = useState({ show: false, type: 'website', data: null })
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [form, setForm] = useState({ name: '', url: '', icon: '', description: '', category_id: '', is_favorite: false, color: '#667eea', icon_name: 'folder' })
|
||||
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
|
||||
})
|
||||
|
||||
useEffect(() => { loadData() }, [])
|
||||
|
||||
@ -32,8 +37,8 @@ function App() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [c, w] = await Promise.all([categoryApi.getAll(), websiteApi.getAll()])
|
||||
setCategories(c.data)
|
||||
setWebsites(w.data)
|
||||
setCategories(c.data || [])
|
||||
setWebsites(w.data || [])
|
||||
} catch (e) { console.error(e) }
|
||||
setLoading(false)
|
||||
}
|
||||
@ -41,7 +46,10 @@ function App() {
|
||||
// 过滤网站
|
||||
const filteredWebsites = websites.filter(site => {
|
||||
const matchCat = selectedCategory === null || site.category_id === selectedCategory
|
||||
const matchSearch = !searchQuery || site.name.includes(searchQuery) || (site.description && site.description.includes(searchQuery))
|
||||
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
|
||||
})
|
||||
|
||||
@ -55,9 +63,14 @@ function App() {
|
||||
// 打开弹窗
|
||||
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 })
|
||||
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' } : { name: '', icon_name: 'folder', color: '#667eea' })
|
||||
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 })
|
||||
}
|
||||
@ -71,7 +84,12 @@ function App() {
|
||||
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 }
|
||||
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)
|
||||
}
|
||||
closeModal()
|
||||
@ -135,7 +153,12 @@ function App() {
|
||||
</div>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<button className="add-btn" onClick={() => openModal('website')}><Icon name="plus" size={14}/><span>添加网站</span></button>
|
||||
<button className="add-btn" onClick={() => openModal('category')} style={{ marginBottom: '8px' }}>
|
||||
<Icon name="folder" size={14}/><span>管理分类</span>
|
||||
</button>
|
||||
<button className="add-btn" onClick={() => openModal('website')}>
|
||||
<Icon name="plus" size={14}/><span>添加网站</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@ -146,7 +169,7 @@ function App() {
|
||||
<Icon name="search" size={18} className="search-icon"/>
|
||||
<input
|
||||
className="search-input"
|
||||
placeholder="搜索网站..."
|
||||
placeholder="搜索网站名称或描述..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
@ -187,9 +210,9 @@ function App() {
|
||||
{filteredWebsites.length === 0 && (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon"><Icon name="globe" size={44}/></div>
|
||||
<div className="empty-title">暂无网站</div>
|
||||
<div className="empty-desc">点击左侧「添加网站」开始收藏你的第一个链接</div>
|
||||
<button className="add-btn" onClick={() => openModal('website')}><Icon name="plus" size={14}/><span>添加网站</span></button>
|
||||
<div className="empty-title">{searchQuery ? '没有找到匹配的网站' : '暂无网站'}</div>
|
||||
<div className="empty-desc">{searchQuery ? '尝试其他关键词' : '点击左侧「添加网站」开始收藏你的第一个链接'}</div>
|
||||
{!searchQuery && <button className="add-btn" onClick={() => openModal('website')}><Icon name="plus" size={14}/><span>添加网站</span></button>}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
@ -215,7 +238,7 @@ function App() {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">描述</label>
|
||||
<input className="form-input" value={form.description} onChange={e => setForm({...form, description: e.target.value})} placeholder="简短描述"/>
|
||||
<input className="form-input" value={form.description || ''} onChange={e => setForm({...form, description: e.target.value})} placeholder="简短描述"/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">分类</label>
|
||||
@ -237,16 +260,32 @@ function App() {
|
||||
<label className="form-label">名称 *</label>
|
||||
<input className="form-input" value={form.name} onChange={e => setForm({...form, name: e.target.value})} placeholder="分类名称"/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">排序(数字越小越靠前)</label>
|
||||
<input type="number" className="form-input" value={form.sort_order} onChange={e => setForm({...form, sort_order: e.target.value})} placeholder="0"/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">颜色</label>
|
||||
<input type="color" className="color-picker" value={form.color} onChange={e => setForm({...form, color: e.target.value})}/>
|
||||
</div>
|
||||
{modal.data && (
|
||||
<div className="form-group">
|
||||
<label className="form-checkbox">
|
||||
<input type="checkbox" onChange={() => {
|
||||
if (confirm('确定删除该分类?该分类下的网站将变为未分类状态。')) {
|
||||
handleDelete('category', modal.data.id)
|
||||
closeModal()
|
||||
}
|
||||
}}/> 删除分类
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="form-actions">
|
||||
<button className="btn btn-cancel" onClick={closeModal}>取消</button>
|
||||
<button className="btn btn-submit" onClick={handleSubmit}>{modal.data ? '保存' : '添加'}</button>
|
||||
<button className="btn btn-submit" onClick={handleSubmit} disabled={!form.name}>{modal.data ? '保存' : '添加'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -259,26 +298,19 @@ function App() {
|
||||
function SiteCard({ site, onClick, onToggleFav, onEdit, onDelete }) {
|
||||
const handleDelete = (e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
onDelete()
|
||||
}
|
||||
|
||||
const handleEdit = (e) => {
|
||||
e.stopPropagation()
|
||||
onEdit()
|
||||
}
|
||||
|
||||
const handleFav = (e) => {
|
||||
e.stopPropagation()
|
||||
onToggleFav()
|
||||
}
|
||||
|
||||
const handleCardClick = () => {
|
||||
onClick()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="site-card" onClick={handleCardClick}>
|
||||
<div className="site-card" onClick={onClick}>
|
||||
{site.category && <span className="card-category">{site.category.name}</span>}
|
||||
<div className="card-icon">
|
||||
{site.icon && site.icon.startsWith('http') ? <img src={site.icon} alt=""/> : <Icon name="globe" size={26}/>}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user