feat: 添加基金支持导入到分组
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
78
app/page.jsx
78
app/page.jsx
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user