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