'use client'; import { useEffect, useState, useMemo, useRef } from 'react'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion'; import { fetchMarketIndices } from '@/app/api/fund'; import { ChevronRightIcon } from 'lucide-react'; import { SettingsIcon } from './Icons'; import { cn } from '@/lib/utils'; import MarketSettingModal from './MarketSettingModal'; /** 简单伪随机,用于稳定迷你图形状 */ function seeded(seed) { return () => { seed = (seed * 9301 + 49297) % 233280; return seed / 233280; }; } /** 迷你走势:优先展示当日分时数据,失败时退回占位折线 */ function MiniTrendLine({ changePercent, code, className }) { const isDown = changePercent <= 0; const width = 80; const height = 28; const pad = 3; const innerH = height - 2 * pad; const innerW = width - 2 * pad; // 占位伪走势(无真实历史数据) const fallbackPath = useMemo(() => { const points = 12; const rnd = seeded(Math.abs(Math.floor(changePercent * 100)) + 1); const arr = Array.from({ length: points }, (_, i) => { const t = i / (points - 1); const x = pad + t * innerW; const y = isDown ? pad + innerH * (1 - t * 0.6) - (rnd() * 4 - 2) : pad + innerH * (0.4 + t * 0.6) + (rnd() * 4 - 2); return [x, Math.max(pad, Math.min(height - pad, y))]; }); return arr.map(([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x} ${y}`).join(' '); }, [changePercent, isDown, innerH, innerW, pad, height]); // 当日分时真实走势 path const [realPath, setRealPath] = useState(null); useEffect(() => { if (!code || typeof window === 'undefined' || typeof document === 'undefined') { setRealPath(null); return; } let cancelled = false; const varName = `min_data_${code}`; const url = `https://web.ifzq.gtimg.cn/appstock/app/minute/query?_var=${varName}&code=${code}&_=${Date.now()}`; const script = document.createElement('script'); script.src = url; script.async = true; const cleanup = () => { if (document.body && document.body.contains(script)) { document.body.removeChild(script); } try { if (window[varName]) { delete window[varName]; } } catch (e) { // ignore } }; script.onload = () => { if (cancelled) { cleanup(); return; } try { const raw = window[varName]; const series = raw && raw.data && raw.data[code] && raw.data[code].data && Array.isArray(raw.data[code].data.data) ? raw.data[code].data.data : null; if (!series || !series.length) { setRealPath(null); return; } // 解析 "HHMM price volume amount" 行,只关心 price const points = series .map((row) => { const parts = String(row).split(' '); const price = parseFloat(parts[1]); if (!Number.isFinite(price)) return null; return { price }; }) .filter(Boolean); if (!points.length) { setRealPath(null); return; } const minP = points.reduce((m, p) => (p.price < m ? p.price : m), points[0].price); const maxP = points.reduce((m, p) => (p.price > m ? p.price : m), points[0].price); const span = maxP - minP || 1; const n = points.length; const pathPoints = points.map((p, idx) => { const t = n > 1 ? idx / (n - 1) : 0; const x = pad + t * innerW; const norm = (p.price - minP) / span; const y = pad + (1 - norm) * innerH; return [x, Math.max(pad, Math.min(height - pad, y))]; }); const d = pathPoints .map(([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x} ${y}`) .join(' '); setRealPath(d); } finally { cleanup(); } }; script.onerror = () => { if (!cancelled) { setRealPath(null); } cleanup(); }; document.body.appendChild(script); return () => { cancelled = true; cleanup(); }; }, [code, height, innerH, innerW, pad]); const d = realPath || fallbackPath; return ( ); } function IndexCard({ item }) { const isUp = item.change >= 0; const colorClass = isUp ? 'text-[var(--danger)]' : 'text-[var(--success)]'; return (
{item.name}
{item.price.toFixed(2)}
{(item.change >= 0 ? '+' : '') + item.change.toFixed(2)}{' '} {(item.changePercent >= 0 ? '+' : '') + item.changePercent.toFixed(2)}%
); } // 默认展示:上证指数、深证成指、创业板指 const DEFAULT_SELECTED_CODES = ['sh000001', 'sz399001', 'sz399006']; export default function MarketIndexAccordion({ navbarHeight = 0, onHeightChange, isMobile, onCustomSettingsChange, refreshing = false, }) { const [indices, setIndices] = useState([]); const [loading, setLoading] = useState(true); const [openValue, setOpenValue] = useState(''); const [selectedCodes, setSelectedCodes] = useState([]); const [settingOpen, setSettingOpen] = useState(false); const [tickerIndex, setTickerIndex] = useState(0); const rootRef = useRef(null); const hasInitializedSelectedCodes = useRef(false); useEffect(() => { const el = rootRef.current; if (!el || typeof onHeightChange !== 'function') return; const ro = new ResizeObserver((entries) => { const entry = entries[0]; if (entry) onHeightChange(entry.contentRect.height); }); ro.observe(el); onHeightChange(el.getBoundingClientRect().height); return () => { ro.disconnect(); onHeightChange(0); }; }, [onHeightChange, loading, indices.length]); const loadIndices = () => { let cancelled = false; setLoading(true); fetchMarketIndices() .then((data) => { if (!cancelled) setIndices(Array.isArray(data) ? data : []); }) .catch(() => { if (!cancelled) setIndices([]); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }; useEffect(() => { // 初次挂载时加载一次指数 const cleanup = loadIndices(); return cleanup; }, []); useEffect(() => { // 跟随基金刷新节奏:每次开始刷新时重新拉取指数 if (!refreshing) return; const cleanup = loadIndices(); return cleanup; }, [refreshing]); // 初始化选中指数(本地偏好 > 默认集合) useEffect(() => { if (!indices.length || typeof window === 'undefined') return; if (hasInitializedSelectedCodes.current) return; try { const stored = window.localStorage.getItem('marketIndexSelected'); const availableCodes = new Set(indices.map((it) => it.code)); if (stored) { const parsed = JSON.parse(stored); if (Array.isArray(parsed)) { const filtered = parsed.filter((c) => availableCodes.has(c)); if (filtered.length) { setSelectedCodes(filtered); hasInitializedSelectedCodes.current = true; return; } } } const defaults = DEFAULT_SELECTED_CODES.filter((c) => availableCodes.has(c)); setSelectedCodes(defaults.length ? defaults : indices.map((it) => it.code).slice(0, 3)); } catch { setSelectedCodes(indices.map((it) => it.code).slice(0, 3)); } }, [indices]); // 持久化用户选择 useEffect(() => { if (typeof window === 'undefined') return; if (!selectedCodes.length) return; try { // 本地首选 key:独立存储,便于快速读取 window.localStorage.setItem('marketIndexSelected', JSON.stringify(selectedCodes)); // 同步到 customSettings,便于云端同步 const raw = window.localStorage.getItem('customSettings'); const parsed = raw ? JSON.parse(raw) : {}; const next = parsed && typeof parsed === 'object' ? { ...parsed, marketIndexSelected: selectedCodes } : { marketIndexSelected: selectedCodes }; window.localStorage.setItem('customSettings', JSON.stringify(next)); onCustomSettingsChange?.(); } catch { // ignore } }, [selectedCodes]); // 用户已选择的指数列表(按 selectedCodes 顺序) const visibleIndices = selectedCodes.length ? selectedCodes .map((code) => indices.find((it) => it.code === code)) .filter(Boolean) : indices; // 重置 tickerIndex 确保索引合法 useEffect(() => { if (tickerIndex >= visibleIndices.length) { setTickerIndex(0); } }, [visibleIndices.length, tickerIndex]); // 收起状态下轮播展示指数 useEffect(() => { if (!visibleIndices.length) return; if (openValue === 'indices') return; if (visibleIndices.length <= 1) return; const timer = setInterval(() => { setTickerIndex((prev) => (prev + 1) % visibleIndices.length); }, 4000); return () => clearInterval(timer); }, [visibleIndices.length, openValue]); const current = visibleIndices.length === 0 ? null : visibleIndices[openValue === 'indices' ? 0 : tickerIndex]; const isUp = current ? current.change >= 0 : false; const colorClass = isUp ? 'text-[var(--danger)]' : 'text-[var(--success)]'; const topMargin = Number(navbarHeight) || 0; const stickyStyle = { marginTop: topMargin, position: 'sticky', top: topMargin, zIndex: 10, width: isMobile ? 'calc(100% + 24px)' : '100%', marginLeft: isMobile ? -12 : 0, }; if (loading && indices.length === 0) { return (
加载大盘指数…
); } return (
{current ? (
{current.name} {current.price.toFixed(2)} {(current.change >= 0 ? '+' : '') + current.change.toFixed(2)} {(current.changePercent >= 0 ? '+' : '') + current.changePercent.toFixed(2)}%
) : ( 暂无指数数据 )}
{ e.stopPropagation(); setSettingOpen(true); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); setSettingOpen(true); } }} aria-label="指数个性化设置" >
{visibleIndices.map((item, i) => (
))}
setSettingOpen(false)} isMobile={isMobile} indices={indices} selectedCodes={selectedCodes} onChangeSelected={setSelectedCodes} onResetDefault={() => { const availableCodes = new Set(indices.map((it) => it.code)); const defaults = DEFAULT_SELECTED_CODES.filter((c) => availableCodes.has(c)); setSelectedCodes(defaults.length ? defaults : indices.map((it) => it.code).slice(0, 3)); }} />
); }