diff --git a/app/globals.css b/app/globals.css index 747467e..da3699d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -287,6 +287,14 @@ body { white-space: nowrap; text-overflow: ellipsis; } + +.modal .item .name { + white-space: normal; + overflow: visible; + text-overflow: clip; + max-width: none; + word-break: break-word; +} .item .weight { font-weight: 600; color: var(--accent); @@ -615,3 +623,182 @@ body { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } + +/* 搜索相关样式 */ +.search-container { + position: relative; + width: 100%; + z-index: 1000; /* 提升到更高层级,避免被其他元素遮挡 */ +} + +.search-input-wrapper { + position: relative; + display: flex; + align-items: center; + flex-wrap: wrap; +} + +.search-spinner { + position: absolute; + right: 12px; + width: 18px; + height: 18px; + border: 2px solid rgba(255, 255, 255, 0.1); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.search-dropdown { + position: absolute; + top: calc(100% + 8px); + left: 0; + right: 0; + max-height: 320px; + overflow-y: auto; + z-index: 10000; /* 确保全局最高层级 */ + padding: 8px; + background: rgba(15, 23, 42, 0.95) !important; +} + +.search-results { + display: flex; + flex-direction: column; + gap: 4px; +} + +.search-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + border-radius: 10px; + cursor: pointer; + transition: all 0.2s ease; +} + +.search-item:hover:not(.added) { + background: rgba(255, 255, 255, 0.08); +} + +.search-item.selected { + background: rgba(34, 211, 238, 0.1); + border: 1px solid rgba(34, 211, 238, 0.2); +} + +.search-item.added { + cursor: not-allowed; + opacity: 0.6; +} + +.fund-info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.fund-name { + font-size: 14px; + font-weight: 600; +} + +.fund-code { + font-size: 11px; +} + +.checkbox { + width: 20px; + height: 20px; + border-radius: 6px; + border: 2px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.search-item.selected .checkbox { + background: var(--primary); + border-color: var(--primary); +} + +.checked-mark { + width: 10px; + height: 6px; + border-left: 2px solid #05263b; + border-bottom: 2px solid #05263b; + transform: rotate(-45deg) translateY(-1px); +} + +.added-label { + font-size: 11px; + padding: 2px 8px; + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + color: var(--muted); +} + +.no-results { + padding: 20px; + text-align: center; + font-size: 14px; +} + +.selected-funds-bar { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: 12px; +} + +.selected-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.selected-inline-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.fund-chip { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + background: rgba(34, 211, 238, 0.15); + border: 1px solid rgba(34, 211, 238, 0.3); + border-radius: 8px; + font-size: 12px; + color: var(--primary); +} + +.remove-chip { + display: flex; + align-items: center; + justify-content: center; + padding: 2px; + background: transparent; + border: none; + color: var(--primary); + cursor: pointer; + border-radius: 4px; +} + +.remove-chip:hover { + background: rgba(34, 211, 238, 0.2); +} + +.batch-add-button { + width: 100%; +} + +/* 提升添加基金区域的层级,避免父级 stacking context 影响 */ +.add-fund-section { + position: relative; + z-index: 1000; +} diff --git a/app/page.jsx b/app/page.jsx index 9ee5d16..aa9f155 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -197,9 +197,57 @@ function FeedbackModal({ onClose }) { ); } +function AddResultModal({ failures, onClose }) { + return ( + + e.stopPropagation()} + > +
+
+ + 部分基金添加失败 +
+ +
+
+ 未获取到估值数据的基金如下: +
+
+ {failures.map((it, idx) => ( +
+ {it.name || '未知名称'} +
+ #{it.code} +
+
+ ))} +
+
+ +
+
+
+ ); +} + export default function HomePage() { const [funds, setFunds] = useState([]); - const [code, setCode] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const timerRef = useRef(null); @@ -230,6 +278,27 @@ export default function HomePage() { const [feedbackOpen, setFeedbackOpen] = useState(false); const [feedbackNonce, setFeedbackNonce] = useState(0); + // 搜索相关状态 + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [selectedFunds, setSelectedFunds] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const searchTimeoutRef = useRef(null); + const dropdownRef = useRef(null); + const [showDropdown, setShowDropdown] = useState(false); + const [addResultOpen, setAddResultOpen] = useState(false); + const [addFailures, setAddFailures] = useState([]); + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setShowDropdown(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + const toggleFavorite = (code) => { setFavorites(prev => { const next = new Set(prev); @@ -442,6 +511,102 @@ export default function HomePage() { }); }; + const performSearch = async (val) => { + if (!val.trim()) { + setSearchResults([]); + return; + } + setIsSearching(true); + // 使用 JSONP 方式获取数据,添加 callback 参数 + const callbackName = `SuggestData_${Date.now()}`; + const url = `https://fundsuggest.eastmoney.com/FundSearch/api/FundSearchAPI.ashx?m=1&key=${encodeURIComponent(val)}&callback=${callbackName}&_=${Date.now()}`; + + try { + await new Promise((resolve, reject) => { + window[callbackName] = (data) => { + if (data && data.Datas) { + // 过滤出基金类型的数据 (CATEGORY 为 700 是公募基金) + const fundsOnly = data.Datas.filter(d => + d.CATEGORY === 700 || + d.CATEGORY === "700" || + d.CATEGORYDESC === "基金" + ); + setSearchResults(fundsOnly); + } + delete window[callbackName]; + resolve(); + }; + + const script = document.createElement('script'); + script.src = url; + script.async = true; + script.onload = () => { + if (document.body.contains(script)) document.body.removeChild(script); + }; + script.onerror = () => { + if (document.body.contains(script)) document.body.removeChild(script); + delete window[callbackName]; + reject(new Error('搜索请求失败')); + }; + document.body.appendChild(script); + }); + } catch (e) { + console.error('搜索失败', e); + } finally { + setIsSearching(false); + } + }; + + const handleSearchInput = (e) => { + const val = e.target.value; + setSearchTerm(val); + if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); + searchTimeoutRef.current = setTimeout(() => performSearch(val), 300); + }; + + const toggleSelectFund = (fund) => { + setSelectedFunds(prev => { + const exists = prev.find(f => f.CODE === fund.CODE); + if (exists) { + return prev.filter(f => f.CODE !== fund.CODE); + } + return [...prev, fund]; + }); + }; + + const batchAddFunds = async () => { + if (selectedFunds.length === 0) return; + setLoading(true); + setError(''); + + try { + const newFunds = []; + for (const f of selectedFunds) { + if (funds.some(existing => existing.code === f.CODE)) continue; + try { + const data = await fetchFundData(f.CODE); + newFunds.push(data); + } catch (e) { + console.error(`添加基金 ${f.CODE} 失败`, e); + } + } + + if (newFunds.length > 0) { + const updated = dedupeByCode([...newFunds, ...funds]); + setFunds(updated); + localStorage.setItem('funds', JSON.stringify(updated)); + } + + setSelectedFunds([]); + setSearchTerm(''); + setSearchResults([]); + } catch (e) { + setError('批量添加失败'); + } finally { + setLoading(false); + } + }; + const refreshAll = async (codes) => { if (refreshingRef.current) return; refreshingRef.current = true; @@ -479,24 +644,49 @@ export default function HomePage() { }; const addFund = async (e) => { - e.preventDefault(); + e?.preventDefault?.(); setError(''); - const clean = code.trim(); - if (!clean) { - setError('请输入基金编号'); - return; - } - if (funds.some((f) => f.code === clean)) { - setError('该基金已添加'); + const manualTokens = String(searchTerm || '') + .split(/[^0-9A-Za-z]+/) + .map(t => t.trim()) + .filter(t => t.length > 0); + const selectedCodes = Array.from(new Set([ + ...selectedFunds.map(f => f.CODE), + ...manualTokens.filter(t => /^\d{6}$/.test(t)) + ])); + if (selectedCodes.length === 0) { + setError('请输入或选择基金代码'); return; } setLoading(true); try { - const data = await fetchFundData(clean); - const next = [data, ...funds]; - setFunds(next); - localStorage.setItem('funds', JSON.stringify(next)); - setCode(''); + const newFunds = []; + const failures = []; + const nameMap = {}; + selectedFunds.forEach(f => { nameMap[f.CODE] = f.NAME; }); + for (const c of selectedCodes) { + if (funds.some((f) => f.code === c)) continue; + try { + const data = await fetchFundData(c); + newFunds.push(data); + } catch (err) { + failures.push({ code: c, name: nameMap[c] }); + } + } + if (newFunds.length === 0) { + setError('未添加任何新基金'); + } else { + const next = dedupeByCode([...newFunds, ...funds]); + setFunds(next); + localStorage.setItem('funds', JSON.stringify(next)); + } + setSearchTerm(''); + setSelectedFunds([]); + setShowDropdown(false); + if (failures.length > 0) { + setAddFailures(failures); + setAddResultOpen(true); + } } catch (e) { setError(e.message || '添加失败'); } finally { @@ -595,20 +785,85 @@ export default function HomePage() {
添加基金 - 输入基金编号(例如:110022) + 搜索并选择基金(支持名称或代码)
-
- setCode(e.target.value)} - inputMode="numeric" - /> - -
+ +
+
+
+ {selectedFunds.length > 0 && ( +
+ {selectedFunds.map(fund => ( +
+ {fund.NAME} + +
+ ))} +
+ )} + setShowDropdown(true)} + /> + {isSearching &&
} +
+ + + + + {showDropdown && (searchTerm.trim() || searchResults.length > 0) && ( + + {searchResults.length > 0 ? ( +
+ {searchResults.map((fund) => { + const isSelected = selectedFunds.some(f => f.CODE === fund.CODE); + const isAlreadyAdded = funds.some(f => f.code === fund.CODE); + return ( +
{ + if (isAlreadyAdded) return; + toggleSelectFund(fund); + }} + > +
+ {fund.NAME} + #{fund.CODE} | {fund.TYPE} +
+ {isAlreadyAdded ? ( + 已添加 + ) : ( +
+ {isSelected &&
} +
+ )} +
+ ); + })} +
+ ) : searchTerm.trim() && !isSearching ? ( +
未找到相关基金
+ ) : null} + + )} + +
+ + + {error &&
{error}
}
@@ -901,6 +1156,14 @@ export default function HomePage() { /> )} + + {addResultOpen && ( + setAddResultOpen(false)} + /> + )} + {settingsOpen && (
setSettingsOpen(false)}>