feat: 添加基金支持导入到分组

This commit is contained in:
hzm
2026-03-05 21:18:29 +08:00
parent 873728a6a2
commit c08c97d706
3 changed files with 184 additions and 79 deletions

View File

@@ -1,5 +1,6 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { CloseIcon } from './Icons';
@@ -9,8 +10,15 @@ export default function ScanImportConfirmModal({
onClose,
onToggle,
onConfirm,
refreshing
refreshing,
groups = []
}) {
const [selectedGroupId, setSelectedGroupId] = useState('all');
const handleConfirm = () => {
onConfirm(selectedGroupId);
};
return (
<motion.div
className="modal-overlay"
@@ -41,6 +49,7 @@ export default function ScanImportConfirmModal({
未识别到有效的基金代码请尝试更清晰的截图或手动搜索
</div>
) : (
<>
<div className="search-results pending-list" style={{ maxHeight: 320, overflowY: 'auto' }}>
{scannedFunds.map((item) => {
const isSelected = selectedScannedCodes.has(item.code);
@@ -76,10 +85,26 @@ export default function ScanImportConfirmModal({
);
})}
</div>
<div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="muted" style={{ fontSize: 13, whiteSpace: 'nowrap' }}>添加到分组</span>
<select
className="select"
value={selectedGroupId}
onChange={(e) => setSelectedGroupId(e.target.value)}
style={{ flex: 1 }}
>
<option value="all">全部</option>
<option value="fav">自选</option>
{groups.filter(g => g.id !== 'all' && g.id !== 'fav').map(g => (
<option key={g.id} value={g.id}>{g.name}</option>
))}
</select>
</div>
</>
)}
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 12 }}>
<button className="button secondary" onClick={onClose}>取消</button>
<button className="button" onClick={onConfirm} disabled={selectedScannedCodes.size === 0 || refreshing}>确认导入</button>
<button className="button" onClick={handleConfirm} disabled={selectedScannedCodes.size === 0 || refreshing}>确认导入</button>
</div>
</motion.div>
</motion.div>

View File

@@ -3074,3 +3074,77 @@ input[type="number"] {
margin: 16px;
}
}
/* ========== 下拉选择框样式 ========== */
.select {
padding: 8px 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: rgba(11, 18, 32, 0.9);
color: var(--text);
font-size: 14px;
cursor: pointer;
outline: none;
transition: border-color 200ms ease, box-shadow 200ms ease;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 36px;
}
.select:hover {
border-color: var(--accent);
}
.select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2);
}
.select option {
background: var(--card);
color: var(--text);
padding: 8px;
}
.select option:hover,
.select option:checked {
background: rgba(34, 211, 238, 0.15);
color: var(--primary);
}
/* 亮色主题:下拉选择框 */
[data-theme="light"] .select {
background-color: rgba(255, 255, 255, 0.95);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
border-color: var(--border);
}
[data-theme="light"] .select:hover {
border-color: var(--accent);
background: linear-gradient(180deg, #fff, #f8fafc);
}
[data-theme="light"] .select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
[data-theme="light"] .select option {
background: #fff;
color: var(--text);
}
[data-theme="light"] .select option {
background: #fff;
color: var(--text);
}
[data-theme="light"] .select option:hover,
[data-theme="light"] .select option:checked {
background: rgba(8, 145, 178, 0.12);
color: var(--primary);
}

View File

@@ -1407,7 +1407,7 @@ export default function HomePage() {
});
};
const confirmScanImport = async () => {
const confirmScanImport = async (targetGroupId = 'all') => {
const codes = Array.from(selectedScannedCodes);
if (codes.length === 0) {
showToast('请至少选择一个基金代码', 'error');
@@ -1451,6 +1451,34 @@ export default function HomePage() {
}
});
if (Object.keys(nextSeries).length > 0) setValuationSeries(prev => ({ ...prev, ...nextSeries }));
if (targetGroupId === 'fav') {
setFavorites(prev => {
const next = new Set(prev);
codes.forEach(code => next.add(code));
storageHelper.setItem('favorites', JSON.stringify(Array.from(next)));
return next;
});
setCurrentTab('fav');
} else if (targetGroupId && targetGroupId !== 'all') {
setGroups(prev => {
const updated = prev.map(g => {
if (g.id === targetGroupId) {
return {
...g,
codes: Array.from(new Set([...g.codes, ...codes]))
};
}
return g;
});
storageHelper.setItem('groups', JSON.stringify(updated));
return updated;
});
setCurrentTab(targetGroupId);
} else {
setCurrentTab('all');
}
setSuccessModal({ open: true, message: `成功导入 ${successCount} 个基金` });
} else {
if (codes.length > 0 && successCount === 0 && failedCount === 0) {
@@ -2534,49 +2562,26 @@ export default function HomePage() {
setError('请输入或选择基金代码');
return;
}
setLoading(true);
try {
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);
storageHelper.setItem('funds', JSON.stringify(next));
const nextSeries = {};
newFunds.forEach(u => {
if (u?.code != null && !u.noValuation && Number.isFinite(Number(u.gsz))) {
nextSeries[u.code] = recordValuation(u.code, { gsz: u.gsz, gztime: u.gztime });
}
});
if (Object.keys(nextSeries).length > 0) setValuationSeries(prev => ({ ...prev, ...nextSeries }));
const fundsToConfirm = selectedCodes.map(code => ({
code,
name: nameMap[code] || '',
status: funds.some(f => f.code === code) ? 'added' : 'pending'
}));
const pendingCodes = fundsToConfirm.filter(f => f.status === 'pending').map(f => f.code);
if (pendingCodes.length === 0) {
setError('所选基金已全部添加');
return;
}
setScannedFunds(fundsToConfirm);
setSelectedScannedCodes(new Set(pendingCodes));
setScanConfirmModalOpen(true);
setSearchTerm('');
setSelectedFunds([]);
setShowDropdown(false);
inputRef.current?.blur();
setIsSearchFocused(false);
if (failures.length > 0) {
setAddFailures(failures);
setAddResultOpen(true);
}
} catch (e) {
setError(e.message || '添加失败');
} finally {
setLoading(false);
}
};
const removeFund = (removeCode) => {
@@ -4731,6 +4736,7 @@ export default function HomePage() {
onToggle={toggleScannedCode}
onConfirm={confirmScanImport}
refreshing={refreshing}
groups={groups}
/>
)}
</AnimatePresence>