add: 添加全部和自选功能

This commit is contained in:
hzm
2026-02-01 12:09:41 +08:00
parent d9defcd87e
commit 8af3445d2d
3 changed files with 138 additions and 1 deletions

View File

@@ -10,6 +10,7 @@
- **纯前端运行**:采用 JSONP 方案直连东方财富、腾讯财经等公开接口,彻底解决跨域问题,支持在 GitHub Pages 等静态环境直接部署。
- **本地持久化**:使用 `localStorage` 存储已添加的基金列表及配置信息,刷新不丢失。
- **响应式设计**:完美适配 PC 与移动端。针对移动端优化了文字展示、间距及交互体验。
- **自选功能**:支持将基金添加至“自选”列表,通过 Tab 切换展示全部基金或仅自选基金。自选状态支持持久化及同步清理。
- **可自定义频率**支持设置自动刷新间隔5秒 - 300秒并提供手动刷新按钮。
## 🛠 技术栈

View File

@@ -336,6 +336,72 @@ body {
box-shadow: 0 10px 20px rgba(248,113,113,0.25);
}
.fav-button {
padding: 4px;
margin-right: 4px;
color: var(--muted);
transition: all 0.2s ease;
background: transparent;
border: none;
width: auto;
height: auto;
}
.fav-button:hover {
color: var(--accent);
}
.fav-button.active {
color: var(--accent);
}
.tabs {
display: flex;
gap: 8px;
padding: 4px;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
width: fit-content;
backdrop-filter: blur(8px);
}
@media (max-width: 640px) {
.tabs {
position: sticky;
top: 60px; /* Navbar height (12px*2 + 24px) */
z-index: 40;
width: calc(100% + 32px);
justify-content: center;
background: rgba(15, 23, 42, 0.9);
border-radius: 0;
margin: 0 -16px 16px -16px;
padding: 10px 16px;
border-bottom: 1px solid var(--border);
backdrop-filter: blur(16px);
}
}
.tab {
padding: 8px 20px;
border-radius: 8px;
border: none;
background: transparent;
color: var(--muted);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.tab:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.05);
}
.tab.active {
background: var(--primary);
color: white;
box-shadow: 0 4px 12px rgba(34, 211, 238, 0.3);
}
.modal-overlay {
position: fixed;
inset: 0;

View File

@@ -49,6 +49,14 @@ function ChevronIcon(props) {
);
}
function StarIcon({ filled, ...props }) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill={filled ? "var(--accent)" : "none"}>
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" stroke="var(--accent)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function Stat({ label, value, delta }) {
const dir = delta > 0 ? 'up' : delta < 0 ? 'down' : '';
return (
@@ -82,6 +90,24 @@ export default function HomePage() {
// 收起/展开状态
const [collapsedCodes, setCollapsedCodes] = useState(new Set());
// 自选状态
const [favorites, setFavorites] = useState(new Set());
const [currentTab, setCurrentTab] = useState('all');
const toggleFavorite = (code) => {
setFavorites(prev => {
const next = new Set(prev);
if (next.has(code)) {
next.delete(code);
} else {
next.add(code);
}
localStorage.setItem('favorites', JSON.stringify(Array.from(next)));
if (next.size === 0) setCurrentTab('all');
return next;
});
};
const toggleCollapse = (code) => {
setCollapsedCodes(prev => {
const next = new Set(prev);
@@ -113,6 +139,11 @@ export default function HomePage() {
if (Array.isArray(savedCollapsed)) {
setCollapsedCodes(new Set(savedCollapsed));
}
// 加载自选状态
const savedFavorites = JSON.parse(localStorage.getItem('favorites') || '[]');
if (Array.isArray(savedFavorites)) {
setFavorites(new Set(savedFavorites));
}
} catch {}
}, []);
@@ -319,6 +350,16 @@ export default function HomePage() {
localStorage.setItem('collapsedCodes', JSON.stringify(Array.from(nextSet)));
return nextSet;
});
// 同步删除自选状态
setFavorites(prev => {
if (!prev.has(removeCode)) return prev;
const nextSet = new Set(prev);
nextSet.delete(removeCode);
localStorage.setItem('favorites', JSON.stringify(Array.from(nextSet)));
if (nextSet.size === 0) setCurrentTab('all');
return nextSet;
});
};
const manualRefresh = async () => {
@@ -404,15 +445,44 @@ export default function HomePage() {
</div>
<div className="col-12">
{funds.length > 0 && favorites.size > 0 && (
<div className="tabs" style={{ marginBottom: 16 }}>
<button
className={`tab ${currentTab === 'all' ? 'active' : ''}`}
onClick={() => setCurrentTab('all')}
>
全部 ({funds.length})
</button>
<button
className={`tab ${currentTab === 'fav' ? 'active' : ''}`}
onClick={() => setCurrentTab('fav')}
>
自选 ({favorites.size})
</button>
</div>
)}
{funds.length === 0 ? (
<div className="glass card empty">尚未添加基金</div>
) : (
<div className="grid">
{funds.map((f) => (
{funds
.filter(f => currentTab === 'all' || favorites.has(f.code))
.map((f) => (
<div key={f.code} className="col-6">
<div className="glass card">
<div className="row" style={{ marginBottom: 10 }}>
<div className="title">
<button
className={`icon-button fav-button ${favorites.has(f.code) ? 'active' : ''}`}
onClick={(e) => {
e.stopPropagation();
toggleFavorite(f.code);
}}
title={favorites.has(f.code) ? "取消自选" : "添加自选"}
>
<StarIcon width="18" height="18" filled={favorites.has(f.code)} />
</button>
<span>{f.name}</span>
<span className="muted">#{f.code}</span>
</div>