feat:表格列排序
This commit is contained in:
@@ -23,7 +23,25 @@ import {
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import ConfirmModal from './ConfirmModal';
|
||||
import { DragIcon, ExitIcon, ResetIcon, SettingsIcon, StarIcon, TrashIcon } from './Icons';
|
||||
import PcTableSettingModal from './PcTableSettingModal';
|
||||
import { DragIcon, ExitIcon, SettingsIcon, StarIcon, TrashIcon } from './Icons';
|
||||
|
||||
const NON_FROZEN_COLUMN_IDS = [
|
||||
'navOrEstimate',
|
||||
'yesterdayChangePercent',
|
||||
'estimateChangePercent',
|
||||
'holdingAmount',
|
||||
'todayProfit',
|
||||
'holdingProfit',
|
||||
];
|
||||
const COLUMN_HEADERS = {
|
||||
navOrEstimate: '净值/估值',
|
||||
yesterdayChangePercent: '昨日涨跌幅',
|
||||
estimateChangePercent: '估值涨跌幅',
|
||||
holdingAmount: '持仓金额',
|
||||
todayProfit: '当日收益',
|
||||
holdingProfit: '持有收益',
|
||||
};
|
||||
|
||||
const SortableRowContext = createContext({
|
||||
setActivatorNodeRef: null,
|
||||
@@ -168,6 +186,69 @@ export default function PcFundTable({
|
||||
} catch { }
|
||||
};
|
||||
|
||||
const getStoredColumnOrder = () => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
try {
|
||||
const raw = window.localStorage.getItem('customSettings');
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
const order = parsed?.pcTableColumnOrder;
|
||||
if (!Array.isArray(order) || order.length === 0) return null;
|
||||
const valid = order.filter((id) => NON_FROZEN_COLUMN_IDS.includes(id));
|
||||
const missing = NON_FROZEN_COLUMN_IDS.filter((id) => !valid.includes(id));
|
||||
return [...valid, ...missing];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const persistColumnOrder = (nextOrder) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
const raw = window.localStorage.getItem('customSettings');
|
||||
const parsed = raw ? JSON.parse(raw) : {};
|
||||
const nextSettings =
|
||||
parsed && typeof parsed === 'object'
|
||||
? { ...parsed, pcTableColumnOrder: nextOrder }
|
||||
: { pcTableColumnOrder: nextOrder };
|
||||
window.localStorage.setItem('customSettings', JSON.stringify(nextSettings));
|
||||
} catch { }
|
||||
};
|
||||
|
||||
const getStoredColumnVisibility = () => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
try {
|
||||
const raw = window.localStorage.getItem('customSettings');
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
const visibility = parsed?.pcTableColumnVisibility;
|
||||
if (!visibility || typeof visibility !== 'object') return null;
|
||||
const normalized = {};
|
||||
NON_FROZEN_COLUMN_IDS.forEach((id) => {
|
||||
const value = visibility[id];
|
||||
if (typeof value === 'boolean') {
|
||||
normalized[id] = value;
|
||||
}
|
||||
});
|
||||
return Object.keys(normalized).length ? normalized : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const persistColumnVisibility = (nextVisibility) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
const raw = window.localStorage.getItem('customSettings');
|
||||
const parsed = raw ? JSON.parse(raw) : {};
|
||||
const nextSettings =
|
||||
parsed && typeof parsed === 'object'
|
||||
? { ...parsed, pcTableColumnVisibility: nextVisibility }
|
||||
: { pcTableColumnVisibility: nextVisibility };
|
||||
window.localStorage.setItem('customSettings', JSON.stringify(nextSettings));
|
||||
} catch { }
|
||||
};
|
||||
|
||||
const [columnSizing, setColumnSizing] = useState(() => {
|
||||
const stored = getStoredColumnSizing();
|
||||
if (stored.actions) {
|
||||
@@ -176,12 +257,45 @@ export default function PcFundTable({
|
||||
}
|
||||
return stored;
|
||||
});
|
||||
const [columnOrder, setColumnOrder] = useState(() => getStoredColumnOrder() ?? [...NON_FROZEN_COLUMN_IDS]);
|
||||
const [columnVisibility, setColumnVisibility] = useState(() => {
|
||||
const stored = getStoredColumnVisibility();
|
||||
if (stored) return stored;
|
||||
const allVisible = {};
|
||||
NON_FROZEN_COLUMN_IDS.forEach((id) => {
|
||||
allVisible[id] = true;
|
||||
});
|
||||
return allVisible;
|
||||
});
|
||||
const [settingModalOpen, setSettingModalOpen] = useState(false);
|
||||
const [resetConfirmOpen, setResetConfirmOpen] = useState(false);
|
||||
const handleResetSizing = () => {
|
||||
setColumnSizing({});
|
||||
persistColumnSizing({});
|
||||
setResetConfirmOpen(false);
|
||||
};
|
||||
|
||||
const handleResetColumnOrder = () => {
|
||||
const defaultOrder = [...NON_FROZEN_COLUMN_IDS];
|
||||
setColumnOrder(defaultOrder);
|
||||
persistColumnOrder(defaultOrder);
|
||||
};
|
||||
|
||||
const handleResetColumnVisibility = () => {
|
||||
const allVisible = {};
|
||||
NON_FROZEN_COLUMN_IDS.forEach((id) => {
|
||||
allVisible[id] = true;
|
||||
});
|
||||
setColumnVisibility(allVisible);
|
||||
persistColumnVisibility(allVisible);
|
||||
};
|
||||
const handleToggleColumnVisibility = (columnId, visible) => {
|
||||
setColumnVisibility((prev = {}) => {
|
||||
const next = { ...prev, [columnId]: visible };
|
||||
persistColumnVisibility(next);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const onRemoveFundRef = useRef(onRemoveFund);
|
||||
const onToggleFavoriteRef = useRef(onToggleFavorite);
|
||||
const onRemoveFromGroupRef = useRef(onRemoveFromGroup);
|
||||
@@ -463,12 +577,12 @@ export default function PcFundTable({
|
||||
className="icon-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation?.();
|
||||
setResetConfirmOpen(true);
|
||||
setSettingModalOpen(true);
|
||||
}}
|
||||
title="重置列宽"
|
||||
title="个性化设置"
|
||||
style={{ border: 'none', width: '24px', height: '24px', backgroundColor: 'transparent', color: 'var(--text)' }}
|
||||
>
|
||||
<ResetIcon width="14" height="14" />
|
||||
<SettingsIcon width="14" height="14" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
@@ -531,6 +645,22 @@ export default function PcFundTable({
|
||||
},
|
||||
state: {
|
||||
columnSizing,
|
||||
columnOrder,
|
||||
columnVisibility,
|
||||
},
|
||||
onColumnOrderChange: (updater) => {
|
||||
setColumnOrder((prev) => {
|
||||
const next = typeof updater === 'function' ? updater(prev) : prev;
|
||||
persistColumnOrder(next);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
onColumnVisibilityChange: (updater) => {
|
||||
setColumnVisibility((prev = {}) => {
|
||||
const next = typeof updater === 'function' ? updater(prev) : (updater || {});
|
||||
persistColumnVisibility(next);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
initialState: {
|
||||
columnPinning: {
|
||||
@@ -752,6 +882,20 @@ export default function PcFundTable({
|
||||
confirmText="重置"
|
||||
/>
|
||||
)}
|
||||
<PcTableSettingModal
|
||||
open={settingModalOpen}
|
||||
onClose={() => setSettingModalOpen(false)}
|
||||
columns={columnOrder.map((id) => ({ id, header: COLUMN_HEADERS[id] ?? id }))}
|
||||
onColumnReorder={(newOrder) => {
|
||||
setColumnOrder(newOrder);
|
||||
persistColumnOrder(newOrder);
|
||||
}}
|
||||
columnVisibility={columnVisibility}
|
||||
onToggleColumnVisibility={handleToggleColumnVisibility}
|
||||
onResetColumnOrder={handleResetColumnOrder}
|
||||
onResetColumnVisibility={handleResetColumnVisibility}
|
||||
onResetSizing={() => setResetConfirmOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { SettingsIcon } from './Icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ConfirmModal from './ConfirmModal';
|
||||
import { ResetIcon, SettingsIcon } from './Icons';
|
||||
|
||||
export default function SettingsModal({
|
||||
onClose,
|
||||
@@ -10,15 +12,38 @@ export default function SettingsModal({
|
||||
exportLocalData,
|
||||
importFileRef,
|
||||
handleImportFileChange,
|
||||
importMsg
|
||||
importMsg,
|
||||
isMobile,
|
||||
containerWidth = 1200,
|
||||
setContainerWidth,
|
||||
onResetContainerWidth,
|
||||
}) {
|
||||
const [sliderDragging, setSliderDragging] = useState(false);
|
||||
const [resetWidthConfirmOpen, setResetWidthConfirmOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sliderDragging) return;
|
||||
const onPointerUp = () => setSliderDragging(false);
|
||||
document.addEventListener('pointerup', onPointerUp);
|
||||
document.addEventListener('pointercancel', onPointerUp);
|
||||
return () => {
|
||||
document.removeEventListener('pointerup', onPointerUp);
|
||||
document.removeEventListener('pointercancel', onPointerUp);
|
||||
};
|
||||
}, [sliderDragging]);
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" role="dialog" aria-modal="true" aria-label="设置" onClick={onClose}>
|
||||
<div
|
||||
className={`modal-overlay ${sliderDragging ? 'modal-overlay-translucent' : ''}`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="设置"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div className="glass card modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="title" style={{ marginBottom: 12 }}>
|
||||
<SettingsIcon width="20" height="20" />
|
||||
<span>设置</span>
|
||||
<span className="muted">配置刷新频率</span>
|
||||
</div>
|
||||
|
||||
<div className="form-group" style={{ marginBottom: 16 }}>
|
||||
@@ -53,6 +78,52 @@ export default function SettingsModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isMobile && setContainerWidth && (
|
||||
<div className="form-group" style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
|
||||
<div className="muted" style={{ fontSize: '0.8rem' }}>页面宽度</div>
|
||||
{onResetContainerWidth && (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={() => setResetWidthConfirmOpen(true)}
|
||||
title="重置页面宽度"
|
||||
style={{
|
||||
border: 'none',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
padding: 0,
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--muted)',
|
||||
}}
|
||||
>
|
||||
<ResetIcon width="14" height="14" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<input
|
||||
type="range"
|
||||
min={600}
|
||||
max={2000}
|
||||
step={10}
|
||||
value={Math.min(2000, Math.max(600, Number(containerWidth) || 1200))}
|
||||
onChange={(e) => setContainerWidth(Number(e.target.value))}
|
||||
onPointerDown={() => setSliderDragging(true)}
|
||||
className="page-width-slider"
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 6,
|
||||
accentColor: 'var(--primary)',
|
||||
}}
|
||||
/>
|
||||
<span className="muted" style={{ fontSize: '0.8rem', minWidth: 48 }}>
|
||||
{Math.min(2000, Math.max(600, Number(containerWidth) || 1200))}px
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group" style={{ marginBottom: 16 }}>
|
||||
<div className="muted" style={{ marginBottom: 8, fontSize: '0.8rem' }}>数据导出</div>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
@@ -80,6 +151,18 @@ export default function SettingsModal({
|
||||
<button className="button" onClick={saveSettings} disabled={tempSeconds < 30}>保存并关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
{resetWidthConfirmOpen && onResetContainerWidth && (
|
||||
<ConfirmModal
|
||||
title="重置页面宽度"
|
||||
message="是否重置页面宽度为默认值 1200px?"
|
||||
onConfirm={() => {
|
||||
onResetContainerWidth();
|
||||
setResetWidthConfirmOpen(false);
|
||||
}}
|
||||
onCancel={() => setResetWidthConfirmOpen(false)}
|
||||
confirmText="重置"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user