diff --git a/backend/main.py b/backend/main.py index ad9b0f3..f2b230d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -140,15 +140,15 @@ def get_websites(): # 转换日期时间并添加分类信息 for site in websites: - if site.get('created_at'): + if site.get('created_at') and hasattr(site['created_at'], 'isoformat'): site['created_at'] = site['created_at'].isoformat() - if site.get('updated_at'): + if site.get('updated_at') and hasattr(site['updated_at'], 'isoformat'): site['updated_at'] = site['updated_at'].isoformat() cat_id = site.get('category_id') if cat_id and cat_id in categories: cat = categories[cat_id] - if cat.get('created_at'): + if cat.get('created_at') and hasattr(cat['created_at'], 'isoformat'): cat['created_at'] = cat['created_at'].isoformat() site['category'] = cat else: diff --git a/frontend/index.html b/frontend/index.html index d9a6f11..35e73c3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,9 +5,15 @@ 导航收藏夹 - - - +
diff --git a/frontend/node_modules/.package-lock.json b/frontend/node_modules/.package-lock.json index 6ffa3e0..64292f7 100644 --- a/frontend/node_modules/.package-lock.json +++ b/frontend/node_modules/.package-lock.json @@ -425,6 +425,20 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", diff --git a/frontend/node_modules/@rollup/rollup-linux-x64-musl/README.md b/frontend/node_modules/@rollup/rollup-linux-x64-musl/README.md new file mode 100644 index 0000000..5848a6c --- /dev/null +++ b/frontend/node_modules/@rollup/rollup-linux-x64-musl/README.md @@ -0,0 +1,3 @@ +# `@rollup/rollup-linux-x64-musl` + +This is the **x86_64-unknown-linux-musl** binary for `rollup` diff --git a/frontend/node_modules/@rollup/rollup-linux-x64-musl/package.json b/frontend/node_modules/@rollup/rollup-linux-x64-musl/package.json new file mode 100644 index 0000000..08cdc06 --- /dev/null +++ b/frontend/node_modules/@rollup/rollup-linux-x64-musl/package.json @@ -0,0 +1,25 @@ +{ + "name": "@rollup/rollup-linux-x64-musl", + "version": "4.60.1", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "files": [ + "rollup.linux-x64-musl.node" + ], + "description": "Native bindings for Rollup", + "author": "Lukas Taegert-Atkinson", + "homepage": "https://rollupjs.org/", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/rollup/rollup.git" + }, + "libc": [ + "musl" + ], + "main": "./rollup.linux-x64-musl.node" +} \ No newline at end of file diff --git a/frontend/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node b/frontend/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node new file mode 100644 index 0000000..9f84ffc Binary files /dev/null and b/frontend/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node differ diff --git a/frontend/package.json b/frontend/package.json index b39fe35..aefcbe5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,10 +9,10 @@ "preview": "vite preview" }, "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", + "axios": "^1.6.5", "lucide-react": "^0.312.0", - "axios": "^1.6.5" + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.48", diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..5d3fa17 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/public/fonts/Inter-Bold.woff2 b/frontend/public/fonts/Inter-Bold.woff2 new file mode 100644 index 0000000..2285bc7 --- /dev/null +++ b/frontend/public/fonts/Inter-Bold.woff2 @@ -0,0 +1 @@ +Couldn't find the requested file /fonts/Inter-Bold.woff2 in inter-font. \ No newline at end of file diff --git a/frontend/public/fonts/Inter-Medium.woff2 b/frontend/public/fonts/Inter-Medium.woff2 new file mode 100644 index 0000000..afad75e --- /dev/null +++ b/frontend/public/fonts/Inter-Medium.woff2 @@ -0,0 +1 @@ +Couldn't find the requested file /fonts/Inter-Medium.woff2 in inter-font. \ No newline at end of file diff --git a/frontend/public/fonts/Inter-Regular.woff2 b/frontend/public/fonts/Inter-Regular.woff2 new file mode 100644 index 0000000..150e6ce --- /dev/null +++ b/frontend/public/fonts/Inter-Regular.woff2 @@ -0,0 +1 @@ +Couldn't find the requested file /fonts/Inter-Regular.woff2 in inter-font. \ No newline at end of file diff --git a/frontend/public/fonts/Inter-SemiBold.woff2 b/frontend/public/fonts/Inter-SemiBold.woff2 new file mode 100644 index 0000000..9868bdf --- /dev/null +++ b/frontend/public/fonts/Inter-SemiBold.woff2 @@ -0,0 +1 @@ +Couldn't find the requested file /fonts/Inter-SemiBold.woff2 in inter-font. \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6ce7bd9..e0a2da3 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,21 +1,20 @@ 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, +// SVG 图标 +const Icon = ({ name, size = 16 }) => { + const icons = { + bookmark: , + search: , + plus: , + folder: , + star: , + globe: , + edit: , + trash: , + x: , + } + return icons[name] || icons.globe } function App() { @@ -23,101 +22,71 @@ function App() { 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 [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 [websiteForm, setWebsiteForm] = useState({ - name: '', - url: '', - icon: '', - description: '', - category_id: '', - is_favorite: false, - }) - - const [categoryForm, setCategoryForm] = useState({ - name: '', - icon: 'folder', - color: '#6366f1', - }) - - // 加载数据 - useEffect(() => { - loadData() - }, []) + 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) - } + 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 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 matchCat = selectedCategory === null || site.category_id === selectedCategory + const matchSearch = !searchQuery || site.name.includes(searchQuery) || (site.description && site.description.includes(searchQuery)) + return matchCat && matchSearch }) - // 收藏网站单独显示 - const favoriteWebsites = filteredWebsites.filter(s => s.is_favorite) - const normalWebsites = filteredWebsites.filter(s => !s.is_favorite) + // 收藏和普通分开 + const favorites = filteredWebsites.filter(s => s.is_favorite) + const normal = 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 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 }) + } else { + setForm(data ? { name: data.name, icon_name: data.icon || 'folder', color: data.color || '#667eea' } : { name: '', icon_name: 'folder', color: '#667eea' }) } + setModal({ show: true, type, data }) } - // 更新网站 - const handleUpdateWebsite = async () => { + const closeModal = () => setModal({ ...modal, show: false, data: null }) + + // 提交表单 + const handleSubmit = async () => { try { - await websiteApi.update(editingWebsite.id, { - ...websiteForm, - category_id: websiteForm.category_id || null, - }) - setEditingWebsite(null) - setShowAddModal(false) - resetWebsiteForm() + 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 } + modal.data ? await categoryApi.update(modal.data.id, payload) : await categoryApi.create(payload) + } + closeModal() loadData() - } catch (error) { - console.error('更新网站失败:', error) - } + } catch (e) { console.error(e) } } - // 删除网站 - const handleDeleteWebsite = async (id) => { - if (!confirm('确定要删除这个网站吗?')) return + // 删除 + const handleDelete = async (type, id) => { + if (!confirm('确定删除?')) return try { - await websiteApi.delete(id) + type === 'website' ? await websiteApi.delete(id) : await categoryApi.delete(id) + if (type === 'category' && selectedCategory === id) setSelectedCategory(null) loadData() - } catch (error) { - console.error('删除网站失败:', error) - } + } catch (e) {} } // 切换收藏 @@ -125,512 +94,207 @@ function App() { try { await websiteApi.update(site.id, { is_favorite: !site.is_favorite }) loadData() - } catch (error) { - console.error('切换收藏失败:', error) - } + } catch (e) {} } // 点击网站 - 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 handleClick = async (site) => { + try { await websiteApi.click(site.id) } catch (e) {} + window.open(site.url, '_blank') } - // 重置表单 - 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 - } - return - } - - // 渲染图标选择器 - const renderIconPicker = () => { - const icons = ['folder', 'globe', 'star', 'message-circle', 'code', 'briefcase', 'gamepad-2', 'book-open'] - return ( -
- {icons.map(icon => { - const IconComponent = iconMap[icon] - return ( - - ) - })} -
- ) - } - - if (loading) { - return ( -
-
-
- ) - } + if (loading) return
return ( -
- {/* 头部 */} -
-
-
-
-
- -
-

导航收藏夹

-
- - {/* 搜索 */} -
- - 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" - /> -
- - {/* 操作按钮 */} -
- - -
+
+ {/* 左侧边栏 */} +
-
- {/* 分类标签 */} -
- - {categories.map(cat => { - const IconComponent = iconMap[cat.icon] || Folder - return ( -
- -
- - -
-
- ) - })} +
+
分类
+ +
setSelectedCategory(null)}> +
+ 全部网站 + {websites.length} +
+ + {categories.map(cat => ( +
setSelectedCategory(cat.id)}> +
+ {cat.name} + {getCategoryCount(cat.id)} +
+ ))}
- {/* 收藏网站 */} - {favoriteWebsites.length > 0 && ( -
-

- - 我的收藏 -

-
- {favoriteWebsites.map(site => ( - handleClickWebsite(site)} - onToggleFavorite={() => toggleFavorite(site)} - onEdit={() => openEditWebsite(site)} - onDelete={() => handleDeleteWebsite(site.id)} - getIcon={getWebsiteIcon} - /> +
+ +
+ + + {/* 主内容 */} +
+ {/* 搜索 */} +
+ + setSearchQuery(e.target.value)} + /> +
+ + {/* 收藏 */} + {favorites.length > 0 && ( +
+
+
我的收藏 ({favorites.length})
+
+
+ {favorites.map(site => ( + handleClick(site)} onToggleFav={() => toggleFavorite(site)} onEdit={() => openModal('website', site)} onDelete={() => handleDelete('website', site.id)}/> ))}
-
+
)} {/* 普通网站 */} - {normalWebsites.length > 0 && ( -
-

- {selectedCategory - ? categories.find(c => c.id === selectedCategory)?.name - : '全部网站'} - ({normalWebsites.length}) -

-
- {normalWebsites.map(site => ( - handleClickWebsite(site)} - onToggleFavorite={() => toggleFavorite(site)} - onEdit={() => openEditWebsite(site)} - onDelete={() => handleDeleteWebsite(site.id)} - getIcon={getWebsiteIcon} - /> + {normal.length > 0 && ( +
+
+
+ {selectedCategory ? categories.find(c => c.id === selectedCategory)?.name : '全部网站'} + ({normal.length}) +
+
+
+ {normal.map(site => ( + handleClick(site)} onToggleFav={() => toggleFavorite(site)} onEdit={() => openModal('website', site)} onDelete={() => handleDelete('website', site.id)}/> ))}
-
+
)} {/* 空状态 */} {filteredWebsites.length === 0 && ( -
-
- -
-

暂无网站

-

点击上方按钮添加你的第一个收藏网站

- +
+
+
暂无网站
+
点击左侧「添加网站」开始收藏你的第一个链接
+
)} - {/* 添加/编辑网站弹窗 */} - {showAddModal && ( - { setShowAddModal(false); setEditingWebsite(null); resetWebsiteForm() }}> -

- {editingWebsite ? '编辑网站' : '添加网站'} -

-
{ e.preventDefault(); editingWebsite ? handleUpdateWebsite() : handleAddWebsite() }} className="space-y-4"> -
- - 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" - /> + {/* 弹窗 */} + {modal.show && ( +
+
e.stopPropagation()}> +
+
{modal.data ? '编辑' : '添加'}{modal.type === 'website' ? '网站' : '分类'}
+
-
- - 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" - /> -
-
- - 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" - /> -
-
- - setWebsiteForm({...websiteForm, description: e.target.value})} - className="w-full px-4 py-2.5 bg-[#15181f] border border-[#2a2f3a] rounded-xl text-[#f1f5f9]" - placeholder="简短描述(可选)" - /> -
-
- - -
-
- setWebsiteForm({...websiteForm, is_favorite: e.target.checked})} - className="w-5 h-5 rounded border-[#2a2f3a] bg-[#15181f] text-indigo-500 focus:ring-indigo-500" - /> - -
-
- - -
- - - )} - {/* 添加/编辑分类弹窗 */} - {showCategoryModal && ( - { setShowCategoryModal(false); setEditingCategory(null) }}> -

- {editingCategory ? '编辑分类' : '添加分类'} -

-
{ e.preventDefault(); editingCategory ? handleUpdateCategory() : handleAddCategory() }} className="space-y-4"> -
- - setCategoryForm({...categoryForm, name: e.target.value})} - className="w-full px-4 py-2.5 bg-[#15181f] border border-[#2a2f3a] rounded-xl text-[#f1f5f9]" - placeholder="例如:社交媒体" - /> + {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, color: e.target.value})}/> +
+ + )} + +
+ +
-
- - {renderIconPicker()} -
-
- - setCategoryForm({...categoryForm, color: e.target.value})} - className="w-full h-12 bg-[#15181f] border border-[#2a2f3a] rounded-xl cursor-pointer" - /> -
-
- - -
- - +
+
)}
) } -// 网站卡片组件 -function WebsiteCard({ site, onClick, onToggleFavorite, onEdit, onDelete, getIcon }) { +// 网站卡片 +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 ( -
- {/* 操作按钮 */} -
- - - +
+
+ {site.icon && site.icon.startsWith('http') ? : }
- - {/* 图标 */} -
- {getIcon(site)} -
- - {/* 信息 */} -

{site.name}

- {site.description && ( -

{site.description}

- )} - {site.category && ( - - {site.category.name} - - )} - - {/* 外部链接图标 */} -
- -
-
- ) -} - -// 弹窗组件 -function Modal({ children, onClose }) { - return ( -
-
e.stopPropagation()} - > - {children} +
{site.name}
+ {site.description &&
{site.description}
} + {site.category && {site.category.name}} +
+ + +
) diff --git a/frontend/src/index.css b/frontend/src/index.css index 67b7224..f534499 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,95 +1,761 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --bg-primary: #0d0f12; - --bg-secondary: #15181f; - --bg-card: #1a1d24; - --bg-hover: #1e293b; - --text-primary: #f1f5f9; - --text-secondary: #94a3b8; - --text-muted: #64748b; - --border-color: #2a2f3a; - --accent: #6366f1; - --accent-hover: #818cf8; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html { - font-family: 'Inter', system-ui, -apple-system, sans-serif; -} +/* ===== 精致美观导航网站 v3 ===== */ +* { margin: 0; padding: 0; box-sizing: border-box; } +html, body, #root { height: 100%; } body { - background: var(--bg-primary); - color: var(--text-primary); + font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', -apple-system, sans-serif; + font-size: 14px; + line-height: 1.6; + color: #333; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + -webkit-font-smoothing: antialiased; +} + +/* ===== 整体布局 ===== */ +.app { + display: flex; min-height: 100vh; } -/* Glass effect */ -.glass { - background: rgba(26, 29, 36, 0.8); - backdrop-filter: blur(12px); - border: 1px solid var(--border-color); +/* ===== 左侧边栏 ===== */ +.sidebar { + width: 240px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border-right: 1px solid rgba(255, 255, 255, 0.2); + display: flex; + flex-direction: column; + flex-shrink: 0; } -/* Card hover effect */ -.card-hover { +.sidebar-header { + padding: 20px; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.logo { + display: flex; + align-items: center; + gap: 10px; +} + +.logo-icon { + width: 36px; + height: 36px; + background: linear-gradient(135deg, #667eea, #764ba2); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 16px; +} + +.logo-text { + font-size: 16px; + font-weight: 600; + background: linear-gradient(135deg, #667eea, #764ba2); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +/* 分类列表 */ +.categories-section { + flex: 1; + overflow-y: auto; + padding: 16px 12px; +} + +.categories-title { + font-size: 11px; + font-weight: 600; + color: #999; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 0 8px; + margin-bottom: 8px; +} + +.category-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + margin-bottom: 4px; + color: #666; + font-size: 13px; +} + +.category-item:hover { + background: rgba(102, 126, 234, 0.08); + color: #667eea; +} + +.category-item.active { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +.category-icon { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + background: rgba(0, 0, 0, 0.04); + font-size: 14px; +} + +.category-item.active .category-icon { + background: rgba(255, 255, 255, 0.2); +} + +.category-name { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.category-count { + font-size: 11px; + padding: 2px 6px; + background: rgba(0, 0, 0, 0.06); + border-radius: 10px; +} + +.category-item.active .category-count { + background: rgba(255, 255, 255, 0.2); +} + +/* 侧边栏底部 */ +.sidebar-footer { + padding: 16px; + border-top: 1px solid rgba(0, 0, 0, 0.05); +} + +.add-btn { + width: 100%; + height: 40px; + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border: none; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: all 0.2s ease; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +.add-btn:hover { + transform: translateY(-1px); + box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4); +} + +/* ===== 主内容区 ===== */ +.main-content { + flex: 1; + padding: 32px; + overflow-y: auto; +} + +/* 搜索栏 */ +.search-bar { + max-width: 600px; + margin-bottom: 32px; + position: relative; +} + +.search-input { + width: 100%; + height: 48px; + padding: 0 20px 0 48px; + background: rgba(255, 255, 255, 0.95); + border: 2px solid transparent; + border-radius: 24px; + font-size: 14px; + color: #333; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); transition: all 0.2s ease; } -.card-hover:hover { - transform: translateY(-4px); - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); - border-color: var(--accent); +.search-input:focus { + outline: none; + border-color: rgba(102, 126, 234, 0.5); + box-shadow: 0 4px 25px rgba(102, 126, 234, 0.2); } -/* Scrollbar */ +.search-icon { + position: absolute; + left: 18px; + top: 50%; + transform: translateY(-50%); + color: #999; +} + +/* 区块标题 */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} + +.section-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 600; + color: white; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); +} + +.section-count { + font-size: 13px; + font-weight: 400; + opacity: 0.8; +} + +/* 网站网格 */ +.website-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 20px; +} + +/* 网站卡片 */ +.site-card { + position: relative; + background: white; + border-radius: 16px; + padding: 24px 20px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + overflow: hidden; +} + +.site-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #667eea, #764ba2); + opacity: 0; + transition: opacity 0.3s ease; +} + +.site-card:hover { + transform: translateY(-6px); + box-shadow: 0 16px 50px rgba(102, 126, 234, 0.25); +} + +.site-card:hover::before { + opacity: 1; +} + +.site-card:hover .card-bottom-actions { + opacity: 1; +} + +.card-bottom-actions { + position: absolute; + bottom: 12px; + left: 12px; + right: 12px; + display: flex; + gap: 6px; + justify-content: flex-end; + align-items: center; +} + +.site-card:hover .external-icon { + opacity: 0; +} + +/* 卡片图标 */ +.card-icon { + width: 52px; + height: 52px; + background: linear-gradient(135deg, #f5f7fa, #e4e8ec); + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; + transition: all 0.3s ease; +} + +.site-card:hover .card-icon { + transform: scale(1.05); + background: linear-gradient(135deg, #667eea15, #764ba215); +} + +.card-icon img { + width: 28px; + height: 28px; + object-fit: contain; +} + +.card-icon svg { + width: 26px; + height: 26px; + color: #667eea; +} + +.card-name { + font-size: 14px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.card-desc { + font-size: 12px; + color: #888; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.card-category { + display: inline-block; + margin-top: 12px; + padding: 4px 10px; + background: linear-gradient(135deg, #667eea10, #764ba210); + border-radius: 6px; + font-size: 11px; + font-weight: 500; + color: #667eea; +} + +/* 收藏星星 */ +.card-favorite { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: #f5f5f5; + color: #ccc; + cursor: pointer; + border-radius: 8px; + padding: 0; + transition: all 0.2s ease; +} + +.card-favorite:hover, +.card-favorite.active { + color: #fbbf24; + background: #fef3c7; +} + +/* 操作按钮 */ +.card-actions { + position: absolute; + bottom: 12px; + right: 12px; + display: flex; + gap: 4px; + padding: 4px; + background: rgba(255, 255, 255, 0.95); + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + opacity: 0; + transition: all 0.2s ease; +} + +.action-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: #f5f5f5; + color: #888; + cursor: pointer; + border-radius: 8px; + transition: all 0.2s ease; +} + +.action-btn:hover { + border-color: #667eea; + color: #667eea; + transform: scale(1.05); +} + +.action-btn.delete:hover { + border-color: #ef4444; + color: #ef4444; + background: #fef2f2; +} + +/* 外部链接图标 */ +.external-icon { + position: absolute; + bottom: 14px; + right: 14px; + color: #ccc; + opacity: 0; + transition: all 0.2s ease; +} + +/* 空状态 */ +.empty-state { + text-align: center; + padding: 80px 40px; + background: rgba(255, 255, 255, 0.95); + border-radius: 24px; + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.1); +} + +.empty-icon { + width: 100px; + height: 100px; + margin: 0 auto 24px; + background: linear-gradient(135deg, #667eea15, #764ba215); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.empty-icon svg { + width: 44px; + height: 44px; + color: #667eea; +} + +.empty-title { + font-size: 20px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 8px; +} + +.empty-desc { + font-size: 14px; + color: #888; + margin-bottom: 28px; +} + +/* ===== 弹窗 ===== */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 24px; +} + +.modal { + background: white; + border-radius: 20px; + padding: 28px; + width: 100%; + max-width: 420px; + box-shadow: 0 25px 80px rgba(0, 0, 0, 0.25); +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; +} + +.modal-title { + font-size: 18px; + font-weight: 600; + color: #1a1a1a; +} + +.modal-close { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: #f5f5f5; + color: #888; + cursor: pointer; + border-radius: 8px; + transition: all 0.2s ease; +} + +.modal-close:hover { + background: #eee; + color: #333; +} + +/* ===== 表单 ===== */ +.form-group { + margin-bottom: 18px; +} + +.form-label { + display: block; + font-size: 13px; + font-weight: 500; + color: #555; + margin-bottom: 8px; +} + +.form-input { + width: 100%; + height: 44px; + padding: 0 14px; + border: 2px solid #f0f0f0; + border-radius: 10px; + font-size: 14px; + color: #333; + transition: all 0.2s ease; +} + +.form-input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.form-select { + width: 100%; + height: 44px; + padding: 0 14px; + border: 2px solid #f0f0f0; + border-radius: 10px; + font-size: 14px; + color: #333; + background: white; + cursor: pointer; + transition: all 0.2s ease; +} + +.form-select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.form-checkbox { + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + color: #555; + cursor: pointer; +} + +.form-checkbox input { + width: 18px; + height: 18px; + accent-color: #667eea; +} + +.color-picker { + width: 100%; + height: 44px; + padding: 4px; + border: 2px solid #f0f0f0; + border-radius: 10px; + cursor: pointer; + background: white; +} + +.icon-picker { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.icon-option { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid #f0f0f0; + border-radius: 10px; + background: white; + color: #666; + cursor: pointer; + transition: all 0.2s ease; +} + +.icon-option:hover { + border-color: #667eea; + color: #667eea; +} + +.icon-option.selected { + border-color: #667eea; + background: linear-gradient(135deg, #667eea10, #764ba210); + color: #667eea; +} + +.form-actions { + display: flex; + gap: 12px; + margin-top: 28px; +} + +.form-actions .btn { + flex: 1; + height: 46px; + border-radius: 12px; + font-size: 15px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-cancel { + background: #f5f5f5; + border: none; + color: #666; +} + +.btn-cancel:hover { + background: #eee; +} + +.btn-submit { + background: linear-gradient(135deg, #667eea, #764ba2); + border: none; + color: white; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +.btn-submit:hover { + transform: translateY(-1px); + box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4); +} + +/* ===== 加载 ===== */ +.loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ===== 响应式 ===== */ +@media (max-width: 768px) { + .app { + flex-direction: column; + } + + .sidebar { + width: 100%; + max-height: 40vh; + border-right: none; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + } + + .categories-section { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 12px; + } + + .categories-title { + display: none; + } + + .category-item { + margin: 0; + padding: 8px 12px; + background: rgba(0, 0, 0, 0.04); + border-radius: 8px; + } + + .category-count { + display: none; + } + + .sidebar-footer { + padding: 12px; + } + + .add-btn span { + display: none; + } + + .main-content { + padding: 20px; + } + + .search-bar { + margin-bottom: 20px; + } + + .search-input { + height: 42px; + } + + .website-grid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 14px; + } + + .site-card { + padding: 18px 16px; + } +} + +/* ===== 滚动条 ===== */ ::-webkit-scrollbar { - width: 8px; - height: 8px; + width: 6px; + height: 6px; } ::-webkit-scrollbar-track { - background: var(--bg-secondary); + background: transparent; } ::-webkit-scrollbar-thumb { - background: var(--bg-hover); - border-radius: 4px; + background: rgba(0, 0, 0, 0.15); + border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { - background: #334155; -} - -/* Input focus */ -input:focus, textarea:focus, select:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2); -} - -/* Button press effect */ -.btn-press:active { - transform: scale(0.97); -} - -/* Modal backdrop */ -.modal-backdrop { - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(4px); -} - -/* Gradient text */ -.gradient-text { - background: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + background: rgba(0, 0, 0, 0.25); } diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index ede1a58..7aaedcc 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -8,23 +8,28 @@ export default { extend: { colors: { primary: { - 50: '#f0f9ff', - 100: '#e0f2fe', - 200: '#bae6fd', - 300: '#7dd3fc', - 400: '#38bdf8', - 500: '#0ea5e9', - 600: '#0284c7', - 700: '#0369a1', - 800: '#075985', - 900: '#0c4a6e', + 50: '#faf5ff', + 100: '#f3e8ff', + 200: '#e9d5ff', + 300: '#d8b4fe', + 400: '#c084fc', + 500: '#a855f7', + 600: '#9333ea', + 700: '#7e22ce', + 800: '#6b21a8', + 900: '#581c87', }, - dark: { - 100: '#1e293b', - 200: '#1a1d24', - 300: '#15181f', - 400: '#111318', - 500: '#0d0f12', + surface: { + 50: '#ffffff', + 100: '#fafafa', + 200: '#f4f4f5', + 300: '#e4e4e7', + 400: '#a1a1aa', + 500: '#71717a', + 600: '#52525b', + 700: '#3f3f46', + 800: '#27272a', + 900: '#18181b', } }, fontFamily: { diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 13dbe22..c25aa9d 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -6,6 +6,7 @@ export default defineConfig({ server: { host: '0.0.0.0', port: 5173, + allowedHosts: ['nav.pags.cn', 'localhost', '127.0.0.1'], proxy: { '/api': { target: 'http://localhost:5003',