add:支持批量添加基金
This commit is contained in:
187
app/globals.css
187
app/globals.css
@@ -287,6 +287,14 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal .item .name {
|
||||||
|
white-space: normal;
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: clip;
|
||||||
|
max-width: none;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
.item .weight {
|
.item .weight {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
@@ -615,3 +623,182 @@ body {
|
|||||||
0% { transform: translateX(-100%); }
|
0% { transform: translateX(-100%); }
|
||||||
100% { 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;
|
||||||
|
}
|
||||||
|
|||||||
317
app/page.jsx
317
app/page.jsx
@@ -197,9 +197,57 @@ function FeedbackModal({ onClose }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AddResultModal({ failures, onClose }) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="modal-overlay"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="添加结果"
|
||||||
|
onClick={onClose}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="glass card modal"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="title" style={{ marginBottom: 12, justifyContent: 'space-between' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
|
<SettingsIcon width="20" height="20" />
|
||||||
|
<span>部分基金添加失败</span>
|
||||||
|
</div>
|
||||||
|
<button className="icon-button" onClick={onClose} style={{ border: 'none', background: 'transparent' }}>
|
||||||
|
<CloseIcon width="20" height="20" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="muted" style={{ marginBottom: 12, fontSize: '14px' }}>
|
||||||
|
未获取到估值数据的基金如下:
|
||||||
|
</div>
|
||||||
|
<div className="list">
|
||||||
|
{failures.map((it, idx) => (
|
||||||
|
<div className="item" key={idx}>
|
||||||
|
<span className="name">{it.name || '未知名称'}</span>
|
||||||
|
<div className="values">
|
||||||
|
<span className="badge">#{it.code}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="row" style={{ justifyContent: 'flex-end', marginTop: 16 }}>
|
||||||
|
<button className="button" onClick={onClose}>知道了</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [funds, setFunds] = useState([]);
|
const [funds, setFunds] = useState([]);
|
||||||
const [code, setCode] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const timerRef = useRef(null);
|
const timerRef = useRef(null);
|
||||||
@@ -230,6 +278,27 @@ export default function HomePage() {
|
|||||||
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
||||||
const [feedbackNonce, setFeedbackNonce] = useState(0);
|
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) => {
|
const toggleFavorite = (code) => {
|
||||||
setFavorites(prev => {
|
setFavorites(prev => {
|
||||||
const next = new Set(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) => {
|
const refreshAll = async (codes) => {
|
||||||
if (refreshingRef.current) return;
|
if (refreshingRef.current) return;
|
||||||
refreshingRef.current = true;
|
refreshingRef.current = true;
|
||||||
@@ -479,24 +644,49 @@ export default function HomePage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addFund = async (e) => {
|
const addFund = async (e) => {
|
||||||
e.preventDefault();
|
e?.preventDefault?.();
|
||||||
setError('');
|
setError('');
|
||||||
const clean = code.trim();
|
const manualTokens = String(searchTerm || '')
|
||||||
if (!clean) {
|
.split(/[^0-9A-Za-z]+/)
|
||||||
setError('请输入基金编号');
|
.map(t => t.trim())
|
||||||
return;
|
.filter(t => t.length > 0);
|
||||||
}
|
const selectedCodes = Array.from(new Set([
|
||||||
if (funds.some((f) => f.code === clean)) {
|
...selectedFunds.map(f => f.CODE),
|
||||||
setError('该基金已添加');
|
...manualTokens.filter(t => /^\d{6}$/.test(t))
|
||||||
|
]));
|
||||||
|
if (selectedCodes.length === 0) {
|
||||||
|
setError('请输入或选择基金代码');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await fetchFundData(clean);
|
const newFunds = [];
|
||||||
const next = [data, ...funds];
|
const failures = [];
|
||||||
setFunds(next);
|
const nameMap = {};
|
||||||
localStorage.setItem('funds', JSON.stringify(next));
|
selectedFunds.forEach(f => { nameMap[f.CODE] = f.NAME; });
|
||||||
setCode('');
|
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) {
|
} catch (e) {
|
||||||
setError(e.message || '添加失败');
|
setError(e.message || '添加失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -595,20 +785,85 @@ export default function HomePage() {
|
|||||||
<div className="title" style={{ marginBottom: 12 }}>
|
<div className="title" style={{ marginBottom: 12 }}>
|
||||||
<PlusIcon width="20" height="20" />
|
<PlusIcon width="20" height="20" />
|
||||||
<span>添加基金</span>
|
<span>添加基金</span>
|
||||||
<span className="muted">输入基金编号(例如:110022)</span>
|
<span className="muted">搜索并选择基金(支持名称或代码)</span>
|
||||||
</div>
|
</div>
|
||||||
<form className="form" onSubmit={addFund}>
|
|
||||||
<input
|
<div className="search-container" ref={dropdownRef}>
|
||||||
className="input"
|
<form className="form" onSubmit={addFund}>
|
||||||
placeholder="基金编号"
|
<div className="search-input-wrapper" style={{ flex: 1, gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
value={code}
|
{selectedFunds.length > 0 && (
|
||||||
onChange={(e) => setCode(e.target.value)}
|
<div className="selected-inline-chips">
|
||||||
inputMode="numeric"
|
{selectedFunds.map(fund => (
|
||||||
/>
|
<div key={fund.CODE} className="fund-chip">
|
||||||
<button className="button" type="submit" disabled={loading}>
|
<span>{fund.NAME}</span>
|
||||||
{loading ? '添加中…' : '添加'}
|
<button onClick={() => toggleSelectFund(fund)} className="remove-chip">
|
||||||
</button>
|
<CloseIcon width="14" height="14" />
|
||||||
</form>
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="搜索基金名称或代码..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearchInput}
|
||||||
|
onFocus={() => setShowDropdown(true)}
|
||||||
|
/>
|
||||||
|
{isSearching && <div className="search-spinner" />}
|
||||||
|
</div>
|
||||||
|
<button className="button" type="submit" disabled={loading}>
|
||||||
|
{loading ? '添加中…' : '添加'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{showDropdown && (searchTerm.trim() || searchResults.length > 0) && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
className="search-dropdown glass"
|
||||||
|
>
|
||||||
|
{searchResults.length > 0 ? (
|
||||||
|
<div className="search-results">
|
||||||
|
{searchResults.map((fund) => {
|
||||||
|
const isSelected = selectedFunds.some(f => f.CODE === fund.CODE);
|
||||||
|
const isAlreadyAdded = funds.some(f => f.code === fund.CODE);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={fund.CODE}
|
||||||
|
className={`search-item ${isSelected ? 'selected' : ''} ${isAlreadyAdded ? 'added' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (isAlreadyAdded) return;
|
||||||
|
toggleSelectFund(fund);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="fund-info">
|
||||||
|
<span className="fund-name">{fund.NAME}</span>
|
||||||
|
<span className="fund-code muted">#{fund.CODE} | {fund.TYPE}</span>
|
||||||
|
</div>
|
||||||
|
{isAlreadyAdded ? (
|
||||||
|
<span className="added-label">已添加</span>
|
||||||
|
) : (
|
||||||
|
<div className="checkbox">
|
||||||
|
{isSelected && <div className="checked-mark" />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : searchTerm.trim() && !isSearching ? (
|
||||||
|
<div className="no-results muted">未找到相关基金</div>
|
||||||
|
) : null}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{error && <div className="muted" style={{ marginTop: 8, color: 'var(--danger)' }}>{error}</div>}
|
{error && <div className="muted" style={{ marginTop: 8, color: 'var(--danger)' }}>{error}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -901,6 +1156,14 @@ export default function HomePage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
<AnimatePresence>
|
||||||
|
{addResultOpen && (
|
||||||
|
<AddResultModal
|
||||||
|
failures={addFailures}
|
||||||
|
onClose={() => setAddResultOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
{settingsOpen && (
|
{settingsOpen && (
|
||||||
<div className="modal-overlay" role="dialog" aria-modal="true" aria-label="设置" onClick={() => setSettingsOpen(false)}>
|
<div className="modal-overlay" role="dialog" aria-modal="true" aria-label="设置" onClick={() => setSettingsOpen(false)}>
|
||||||
|
|||||||
Reference in New Issue
Block a user