add: 补充 README 并增加 loading 效果

This commit is contained in:
hzm
2026-01-31 23:24:01 +08:00
parent cd04131929
commit 6fb93e5f3c
3 changed files with 107 additions and 36 deletions

View File

@@ -6,7 +6,7 @@
## ✨ 特性
- **实时估值**:通过输入基金编号,实时获取并展示基金的单位净值、估值净值及实时涨跌幅。
- **重仓追踪**:自动获取基金前 10 大重仓股票,并实时追踪重仓股的盘中涨跌情况。
- **重仓追踪**:自动获取基金前 10 大重仓股票,并实时追踪重仓股的盘中涨跌情况。支持收起/展开展示。
- **纯前端运行**:采用 JSONP 方案直连东方财富、腾讯财经等公开接口,彻底解决跨域问题,支持在 GitHub Pages 等静态环境直接部署。
- **本地持久化**:使用 `localStorage` 存储已添加的基金列表及配置信息,刷新不丢失。
- **响应式设计**:完美适配 PC 与移动端。针对移动端优化了文字展示、间距及交互体验。

View File

@@ -39,7 +39,7 @@ body {
backdrop-filter: blur(8px);
}
.title {
.card .title {
display: flex;
align-items: center;
gap: 12px;
@@ -47,10 +47,6 @@ body {
letter-spacing: 0.2px;
}
.card .title {
flex-wrap: wrap;
}
.card .title span:first-child {
white-space: normal;
word-break: break-word;
@@ -203,27 +199,20 @@ body {
.stat {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
align-items: baseline;
gap: 8px;
}
.stat .label {
font-size: 11px;
font-size: 12px;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat .value {
font-size: 17px;
font-size: 20px;
font-weight: 700;
line-height: 1.2;
white-space: nowrap;
}
.stat .badge {
padding: 2px 6px;
font-size: 10px;
width: fit-content;
padding: 4px 8px;
font-size: 12px;
}
@media (max-width: 640px) {
@@ -236,8 +225,29 @@ body {
.card {
padding: 12px;
}
.stat {
flex-direction: column;
gap: 4px;
min-width: 0;
}
.stat .label {
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat .value {
font-size: 15px;
line-height: 1.2;
white-space: nowrap;
}
.stat .badge {
padding: 2px 6px;
font-size: 10px;
width: fit-content;
}
.card .title {
flex-wrap: wrap;
}
.item .name {
max-width: 100px;
@@ -379,3 +389,20 @@ body {
animation: none;
}
}
.loading-bar {
position: absolute;
top: 0;
left: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--primary), transparent);
width: 100%;
animation: loading 1.5s infinite;
z-index: 100;
border-radius: 16px 16px 0 0;
}
@keyframes loading {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}

View File

@@ -34,13 +34,21 @@ function RefreshIcon(props) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M4 12a8 8 0 0 1 12.5-6.9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
<path d="M16 5h3v3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
<path d="M16 5h3v3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M20 12a8 8 0 0 1-12.5 6.9" stroke="currentColor" strokeWidth="2" />
<path d="M8 19H5v-3" stroke="currentColor" strokeWidth="2" />
</svg>
);
}
function ChevronIcon(props) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M6 9l6 6 6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function Stat({ label, value, delta }) {
const dir = delta > 0 ? 'up' : delta < 0 ? 'down' : '';
return (
@@ -62,13 +70,30 @@ export default function HomePage() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const timerRef = useRef(null);
const [manualRefreshing, setManualRefreshing] = useState(false);
// 刷新频率状态
const [refreshMs, setRefreshMs] = useState(30000);
const [settingsOpen, setSettingsOpen] = useState(false);
const [tempSeconds, setTempSeconds] = useState(30);
// 全局刷新状态
const [refreshing, setRefreshing] = useState(false);
// 收起/展开状态
const [collapsedCodes, setCollapsedCodes] = useState(new Set());
const toggleCollapse = (code) => {
setCollapsedCodes(prev => {
const next = new Set(prev);
if (next.has(code)) {
next.delete(code);
} else {
next.add(code);
}
return next;
});
};
useEffect(() => {
try {
const saved = JSON.parse(localStorage.getItem('funds') || '[]');
@@ -221,6 +246,8 @@ export default function HomePage() {
};
const refreshAll = async (codes) => {
if (refreshing) return;
setRefreshing(true);
try {
// 改用串行请求,避免全局回调 jsonpgz 并发冲突
const updated = [];
@@ -241,6 +268,8 @@ export default function HomePage() {
}
} catch (e) {
console.error(e);
} finally {
setRefreshing(false);
}
};
@@ -277,15 +306,10 @@ export default function HomePage() {
};
const manualRefresh = async () => {
if (manualRefreshing) return;
if (refreshing) return;
const codes = funds.map((f) => f.code);
if (!codes.length) return;
setManualRefreshing(true);
try {
await refreshAll(codes);
} finally {
setManualRefreshing(false);
}
await refreshAll(codes);
};
const saveSettings = (e) => {
@@ -307,6 +331,7 @@ export default function HomePage() {
return (
<div className="container content">
<div className="navbar glass">
{refreshing && <div className="loading-bar"></div>}
<div className="brand">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="var(--accent)" strokeWidth="2" />
@@ -323,11 +348,11 @@ export default function HomePage() {
className="icon-button"
aria-label="立即刷新"
onClick={manualRefresh}
disabled={manualRefreshing || funds.length === 0}
aria-busy={manualRefreshing}
disabled={refreshing || funds.length === 0}
aria-busy={refreshing}
title="立即刷新"
>
<RefreshIcon className={manualRefreshing ? 'spin' : ''} width="18" height="18" />
<RefreshIcon className={refreshing ? 'spin' : ''} width="18" height="18" />
</button>
<button
className="icon-button"
@@ -394,12 +419,31 @@ export default function HomePage() {
<Stat label="估值净值" value={f.gsz ?? '—'} />
<Stat label="涨跌幅" value={typeof f.gszzl === 'number' ? `${f.gszzl.toFixed(2)}%` : f.gszzl ?? '—'} delta={Number(f.gszzl) || 0} />
</div>
<div style={{ marginBottom: 8 }} className="title">
<span>前10重仓股票</span>
<span className="muted">涨跌幅 / 占比</span>
<div
style={{ marginBottom: 8, cursor: 'pointer', userSelect: 'none' }}
className="title"
onClick={() => toggleCollapse(f.code)}
>
<div className="row" style={{ width: '100%', flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span>前10重仓股票</span>
<ChevronIcon
width="16"
height="16"
className="muted"
style={{
transform: collapsedCodes.has(f.code) ? 'rotate(-90deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease'
}}
/>
</div>
<span className="muted">涨跌幅 / 占比</span>
</div>
</div>
{Array.isArray(f.holdings) && f.holdings.length ? (
<div className="list">
<div className={`list ${collapsedCodes.has(f.code) ? 'collapsed' : ''}`} style={{
display: collapsedCodes.has(f.code) ? 'none' : 'grid'
}}>
{f.holdings.map((h, idx) => (
<div className="item" key={idx}>
<span className="name">{h.name}</span>
@@ -415,7 +459,7 @@ export default function HomePage() {
))}
</div>
) : (
<div className="muted">暂无重仓数据</div>
<div className="muted" style={{ display: collapsedCodes.has(f.code) ? 'none' : 'block' }}>暂无重仓数据</div>
)}
</div>
</div>