add:完善分组功能

This commit is contained in:
hzm
2026-02-03 23:07:28 +08:00
parent 073fdcdd7f
commit 13426e5192
2 changed files with 106 additions and 108 deletions

View File

@@ -945,6 +945,12 @@ body {
color: var(--primary); color: var(--primary);
} }
.add-group-row-btn:hover {
border-color: var(--primary) !important;
color: var(--primary) !important;
background: rgba(255,255,255,0.05) !important;
}
.tabs-nav-btn { .tabs-nav-btn {
width: 28px; width: 28px;
height: 28px; height: 28px;

View File

@@ -352,7 +352,15 @@ function GroupManageModal({ groups, onClose, onSave }) {
}; };
const handleDeleteClick = (id, name) => { const handleDeleteClick = (id, name) => {
setDeleteConfirm({ id, name }); const itemToDelete = items.find(it => it.id === id);
const isNew = !groups.find(g => g.id === id);
const isEmpty = itemToDelete && (!itemToDelete.codes || itemToDelete.codes.length === 0);
if (isNew || isEmpty) {
setItems(prev => prev.filter(item => item.id !== id));
} else {
setDeleteConfirm({ id, name });
}
}; };
const handleConfirmDelete = () => { const handleConfirmDelete = () => {
@@ -362,11 +370,24 @@ function GroupManageModal({ groups, onClose, onSave }) {
} }
}; };
const handleAddRow = () => {
const newGroup = {
id: `group_${Date.now()}`,
name: '',
codes: []
};
setItems(prev => [...prev, newGroup]);
};
const handleConfirm = () => { const handleConfirm = () => {
const hasEmpty = items.some(it => !it.name.trim());
if (hasEmpty) return;
onSave(items); onSave(items);
onClose(); onClose();
}; };
const isAllValid = items.every(it => it.name.trim() !== '');
return ( return (
<motion.div <motion.div
className="modal-overlay" className="modal-overlay"
@@ -410,11 +431,17 @@ function GroupManageModal({ groups, onClose, onSave }) {
<DragIcon width="18" height="18" className="muted" /> <DragIcon width="18" height="18" className="muted" />
</div> </div>
<input <input
className="input group-rename-input" className={`input group-rename-input ${!item.name.trim() ? 'error' : ''}`}
value={item.name} value={item.name}
onChange={(e) => handleRename(item.id, e.target.value)} onChange={(e) => handleRename(item.id, e.target.value)}
placeholder="分组名称" placeholder="请输入分组名称..."
style={{ flex: 1, height: '36px', fontSize: '14px', background: 'rgba(0,0,0,0.2)' }} style={{
flex: 1,
height: '36px',
fontSize: '14px',
background: 'rgba(0,0,0,0.2)',
border: !item.name.trim() ? '1px solid var(--danger)' : 'none'
}}
/> />
<button <button
className="icon-button danger" className="icon-button danger"
@@ -428,10 +455,43 @@ function GroupManageModal({ groups, onClose, onSave }) {
))} ))}
</Reorder.Group> </Reorder.Group>
)} )}
<button
className="add-group-row-btn"
onClick={handleAddRow}
style={{
width: '100%',
marginTop: 12,
padding: '10px',
borderRadius: '12px',
border: '1px dashed var(--border)',
background: 'rgba(255,255,255,0.02)',
color: 'var(--muted)',
fontSize: '14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
>
<PlusIcon width="16" height="16" />
<span>新增分组行</span>
</button>
</div> </div>
<div style={{ marginTop: 24 }}> <div style={{ marginTop: 24 }}>
<button className="button" onClick={handleConfirm} style={{ width: '100%' }}> {!isAllValid && (
<div className="error-text" style={{ marginBottom: 12, textAlign: 'center' }}>
所有分组名称均不能为空
</div>
)}
<button
className="button"
onClick={handleConfirm}
disabled={!isAllValid}
style={{ width: '100%', opacity: isAllValid ? 1 : 0.6 }}
>
完成 完成
</button> </button>
</div> </div>
@@ -640,7 +700,6 @@ export default function HomePage() {
const [showDropdown, setShowDropdown] = useState(false); const [showDropdown, setShowDropdown] = useState(false);
const [addResultOpen, setAddResultOpen] = useState(false); const [addResultOpen, setAddResultOpen] = useState(false);
const [addFailures, setAddFailures] = useState([]); const [addFailures, setAddFailures] = useState([]);
const [groupSelectorFund, setGroupSelectorFund] = useState(null); // { code, rect }
const tabsRef = useRef(null); const tabsRef = useRef(null);
// 过滤和排序后的基金列表 // 过滤和排序后的基金列表
@@ -1280,6 +1339,7 @@ export default function HomePage() {
version: 1, version: 1,
funds: JSON.parse(localStorage.getItem('funds') || '[]'), funds: JSON.parse(localStorage.getItem('funds') || '[]'),
favorites: JSON.parse(localStorage.getItem('favorites') || '[]'), favorites: JSON.parse(localStorage.getItem('favorites') || '[]'),
groups: JSON.parse(localStorage.getItem('groups') || '[]'),
collapsedCodes: JSON.parse(localStorage.getItem('collapsedCodes') || '[]'), collapsedCodes: JSON.parse(localStorage.getItem('collapsedCodes') || '[]'),
refreshMs: parseInt(localStorage.getItem('refreshMs') || '30000', 10), refreshMs: parseInt(localStorage.getItem('refreshMs') || '30000', 10),
viewMode: localStorage.getItem('viewMode') || 'card', viewMode: localStorage.getItem('viewMode') || 'card',
@@ -1333,6 +1393,7 @@ export default function HomePage() {
// 从 localStorage 读取最新数据进行合并,防止状态滞后导致的数据丢失 // 从 localStorage 读取最新数据进行合并,防止状态滞后导致的数据丢失
const currentFunds = JSON.parse(localStorage.getItem('funds') || '[]'); const currentFunds = JSON.parse(localStorage.getItem('funds') || '[]');
const currentFavorites = JSON.parse(localStorage.getItem('favorites') || '[]'); const currentFavorites = JSON.parse(localStorage.getItem('favorites') || '[]');
const currentGroups = JSON.parse(localStorage.getItem('groups') || '[]');
const currentCollapsed = JSON.parse(localStorage.getItem('collapsedCodes') || '[]'); const currentCollapsed = JSON.parse(localStorage.getItem('collapsedCodes') || '[]');
let mergedFunds = currentFunds; let mergedFunds = currentFunds;
@@ -1354,6 +1415,24 @@ export default function HomePage() {
localStorage.setItem('favorites', JSON.stringify(mergedFav)); localStorage.setItem('favorites', JSON.stringify(mergedFav));
} }
if (Array.isArray(data.groups)) {
// 合并分组:如果 ID 相同则合并 codes否则添加新分组
const mergedGroups = [...currentGroups];
data.groups.forEach(incomingGroup => {
const existingIdx = mergedGroups.findIndex(g => g.id === incomingGroup.id);
if (existingIdx > -1) {
mergedGroups[existingIdx] = {
...mergedGroups[existingIdx],
codes: Array.from(new Set([...mergedGroups[existingIdx].codes, ...(incomingGroup.codes || [])]))
};
} else {
mergedGroups.push(incomingGroup);
}
});
setGroups(mergedGroups);
localStorage.setItem('groups', JSON.stringify(mergedGroups));
}
if (Array.isArray(data.collapsedCodes)) { if (Array.isArray(data.collapsedCodes)) {
const mergedCollapsed = Array.from(new Set([...currentCollapsed, ...data.collapsedCodes])); const mergedCollapsed = Array.from(new Set([...currentCollapsed, ...data.collapsedCodes]));
setCollapsedCodes(new Set(mergedCollapsed)); setCollapsedCodes(new Set(mergedCollapsed));
@@ -1573,15 +1652,6 @@ export default function HomePage() {
<SortIcon width="16" height="16" /> <SortIcon width="16" height="16" />
</button> </button>
)} )}
{currentTab !== 'all' && currentTab !== 'fav' && (
<button
className="icon-button"
onClick={() => setAddFundToGroupOpen(true)}
title="添加基金到此分组"
>
<FolderPlusIcon width="16" height="16" />
</button>
)}
<button <button
className="icon-button add-group-btn" className="icon-button add-group-btn"
onClick={() => setGroupModalOpen(true)} onClick={() => setGroupModalOpen(true)}
@@ -1650,7 +1720,20 @@ export default function HomePage() {
)} )}
</div> </div>
) : ( ) : (
<AnimatePresence mode="wait"> <>
{currentTab !== 'all' && currentTab !== 'fav' && (
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 12 }}>
<button
className="button"
onClick={() => setAddFundToGroupOpen(true)}
style={{ height: '32px', fontSize: '13px', padding: '0 12px', display: 'flex', alignItems: 'center', gap: '6px' }}
>
<PlusIcon width="16" height="16" />
<span>添加基金</span>
</button>
</div>
)}
<AnimatePresence mode="wait">
<motion.div <motion.div
key={viewMode} key={viewMode}
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
@@ -1715,17 +1798,6 @@ export default function HomePage() {
<span className="muted" style={{ fontSize: '12px' }}>{f.gztime || f.time || '-'}</span> <span className="muted" style={{ fontSize: '12px' }}>{f.gztime || f.time || '-'}</span>
</div> </div>
<div className="table-cell text-center action-cell" style={{ gap: 4 }}> <div className="table-cell text-center action-cell" style={{ gap: 4 }}>
<button
className={`icon-button ${groups.some(g => g.codes.includes(f.code)) ? 'active' : ''}`}
onClick={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
setGroupSelectorFund({ code: f.code, rect });
}}
title="管理分组"
style={{ width: '28px', height: '28px' }}
>
<FolderPlusIcon width="14" height="14" />
</button>
<button <button
className="icon-button danger" className="icon-button danger"
onClick={() => removeFund(f.code)} onClick={() => removeFund(f.code)}
@@ -1762,17 +1834,6 @@ export default function HomePage() {
<strong>{f.gztime || f.time || '-'}</strong> <strong>{f.gztime || f.time || '-'}</strong>
</div> </div>
<div className="row" style={{ gap: 4 }}> <div className="row" style={{ gap: 4 }}>
<button
className={`icon-button ${groups.some(g => g.codes.includes(f.code)) ? 'active' : ''}`}
onClick={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
setGroupSelectorFund({ code: f.code, rect });
}}
title="管理分组"
style={{ width: '28px', height: '28px' }}
>
<FolderPlusIcon width="14" height="14" />
</button>
<button <button
className="icon-button danger" className="icon-button danger"
onClick={() => removeFund(f.code)} onClick={() => removeFund(f.code)}
@@ -1860,6 +1921,7 @@ export default function HomePage() {
</div> </div>
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
</>
)} )}
</div> </div>
</div> </div>
@@ -1932,76 +1994,6 @@ export default function HomePage() {
)} )}
</AnimatePresence> </AnimatePresence>
<AnimatePresence>
{groupSelectorFund && (
<div
className="modal-overlay"
style={{ background: 'transparent' }}
onClick={() => setGroupSelectorFund(null)}
>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: -10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: -10 }}
className="glass card group-selector-popup"
style={{
position: 'fixed',
left: groupSelectorFund.rect.left - 160,
top: groupSelectorFund.rect.top + 35,
width: '200px',
zIndex: 10001,
padding: '8px',
maxHeight: '240px',
overflowY: 'auto'
}}
onClick={(e) => e.stopPropagation()}
>
<div className="muted" style={{ fontSize: '12px', padding: '4px 8px', marginBottom: 4 }}>选择分组</div>
{groups.length === 0 ? (
<div className="muted" style={{ padding: '8px', fontSize: '13px' }}>暂无自定义分组</div>
) : (
<div className="group-list">
{groups.map(g => (
<div
key={g.id}
className={`group-item ${g.codes.includes(groupSelectorFund.code) ? 'selected' : ''}`}
onClick={() => toggleFundInGroup(groupSelectorFund.code, g.id)}
style={{
padding: '8px 12px',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
transition: 'background 0.2s ease'
}}
>
<span>{g.name}</span>
{g.codes.includes(groupSelectorFund.code) && (
<div className="checked-mark" style={{ width: '8px', height: '4px' }} />
)}
</div>
))}
</div>
)}
<div style={{ borderTop: '1px solid var(--border)', marginTop: 4, paddingTop: 4 }}>
<button
className="tab"
style={{ width: '100%', textAlign: 'left', padding: '8px 12px', fontSize: '13px' }}
onClick={() => {
setGroupSelectorFund(null);
setGroupModalOpen(true);
}}
>
+ 新增分组
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
<AnimatePresence> <AnimatePresence>
{successModal.open && ( {successModal.open && (
<SuccessModal <SuccessModal