From c08c97d70630e131884069d7e35d0324fb460338 Mon Sep 17 00:00:00 2001 From: hzm <934585316@qq.com> Date: Thu, 5 Mar 2026 21:18:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=9F=BA=E9=87=91?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=AF=BC=E5=85=A5=E5=88=B0=E5=88=86=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ScanImportConfirmModal.jsx | 97 ++++++++++++++--------- app/globals.css | 74 +++++++++++++++++ app/page.jsx | 92 +++++++++++---------- 3 files changed, 184 insertions(+), 79 deletions(-) diff --git a/app/components/ScanImportConfirmModal.jsx b/app/components/ScanImportConfirmModal.jsx index 826488b..7b4745e 100644 --- a/app/components/ScanImportConfirmModal.jsx +++ b/app/components/ScanImportConfirmModal.jsx @@ -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 ( ) : ( -
- {scannedFunds.map((item) => { - const isSelected = selectedScannedCodes.has(item.code); - const isAlreadyAdded = item.status === 'added'; - const isInvalid = item.status === 'invalid'; - const isDisabled = isAlreadyAdded || isInvalid; - const displayName = item.name || (isInvalid ? '未找到基金' : '未知基金'); - return ( -
e.preventDefault()} - onClick={() => { - if (isDisabled) return; - onToggle(item.code); - }} - style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }} - > -
- {displayName} - #{item.code} -
- {isAlreadyAdded ? ( - 已添加 - ) : isInvalid ? ( - 未找到 - ) : ( -
- {isSelected &&
} + <> +
+ {scannedFunds.map((item) => { + const isSelected = selectedScannedCodes.has(item.code); + const isAlreadyAdded = item.status === 'added'; + const isInvalid = item.status === 'invalid'; + const isDisabled = isAlreadyAdded || isInvalid; + const displayName = item.name || (isInvalid ? '未找到基金' : '未知基金'); + return ( +
e.preventDefault()} + onClick={() => { + if (isDisabled) return; + onToggle(item.code); + }} + style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }} + > +
+ {displayName} + #{item.code}
- )} -
- ); - })} -
+ {isAlreadyAdded ? ( + 已添加 + ) : isInvalid ? ( + 未找到 + ) : ( +
+ {isSelected &&
} +
+ )} +
+ ); + })} +
+
+ 添加到分组: + +
+ )}
- +
diff --git a/app/globals.css b/app/globals.css index a8d7045..3b6e293 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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); +} diff --git a/app/page.jsx b/app/page.jsx index 4d689e4..86b38ce 100644 --- a/app/page.jsx +++ b/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 })); - } - 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 nameMap = {}; + selectedFunds.forEach(f => { nameMap[f.CODE] = f.NAME; }); + 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); }; const removeFund = (removeCode) => { @@ -4731,6 +4736,7 @@ export default function HomePage() { onToggle={toggleScannedCode} onConfirm={confirmScanImport} refreshing={refreshing} + groups={groups} /> )}