diff --git a/app/components/PcFundTable.jsx b/app/components/PcFundTable.jsx index 7835d56..d95289f 100644 --- a/app/components/PcFundTable.jsx +++ b/app/components/PcFundTable.jsx @@ -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)' }} > - + ), @@ -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="重置" /> )} + 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)} + /> ); } diff --git a/app/components/SettingsModal.jsx b/app/components/SettingsModal.jsx index 0413e23..061a8f4 100644 --- a/app/components/SettingsModal.jsx +++ b/app/components/SettingsModal.jsx @@ -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 ( -
+
e.stopPropagation()}>
设置 - 配置刷新频率
@@ -53,6 +78,52 @@ export default function SettingsModal({ )}
+ {!isMobile && setContainerWidth && ( +
+
+
页面宽度
+ {onResetContainerWidth && ( + + )} +
+
+ setContainerWidth(Number(e.target.value))} + onPointerDown={() => setSliderDragging(true)} + className="page-width-slider" + style={{ + flex: 1, + height: 6, + accentColor: 'var(--primary)', + }} + /> + + {Math.min(2000, Math.max(600, Number(containerWidth) || 1200))}px + +
+
+ )} +
数据导出
@@ -80,6 +151,18 @@ export default function SettingsModal({
+ {resetWidthConfirmOpen && onResetContainerWidth && ( + { + onResetContainerWidth(); + setResetWidthConfirmOpen(false); + }} + onCancel={() => setResetWidthConfirmOpen(false)} + confirmText="重置" + /> + )}
); } diff --git a/app/globals.css b/app/globals.css index f5acb59..a6a0315 100644 --- a/app/globals.css +++ b/app/globals.css @@ -92,6 +92,43 @@ body::before { padding: 24px; } +.page-width-slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 6px; + border-radius: 3px; + background: rgba(148, 163, 184, 0.4); + outline: none; +} + +.page-width-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--primary); + cursor: pointer; + border: 2px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); + transition: transform 0.15s; +} + +.page-width-slider::-webkit-slider-thumb:hover { + transform: scale(1.1); +} + +.page-width-slider::-moz-range-thumb { + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--primary); + cursor: pointer; + border: 2px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); +} + .glass { background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02)); border: 1px solid var(--border); @@ -1582,6 +1619,12 @@ input[type="number"] { align-items: center; justify-content: center; z-index: 60; + transition: background 0.2s ease, backdrop-filter 0.2s ease; +} + +.modal-overlay-translucent { + background: rgba(2, 6, 23, 0.15); + backdrop-filter: blur(1px); } .modal { @@ -1590,6 +1633,88 @@ input[type="number"] { padding: 16px; } +/* PC 表格设置侧弹框 */ +.pc-table-setting-overlay { + position: fixed; + inset: 0; + background: rgba(2, 6, 23, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: stretch; + justify-content: flex-end; +} + +.pc-table-setting-drawer { + width: 360px; + max-width: 90vw; + display: flex; + flex-direction: column; + border-radius: 16px 0 0 16px; + border: 1px solid var(--border); + border-right: none; + box-shadow: -8px 0 32px rgba(0, 0, 0, 0.3); +} + +.pc-table-setting-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 20px 16px; + border-bottom: 1px solid var(--border); + font-size: 16px; + font-weight: 600; +} + +.pc-table-setting-body { + flex: 1; + overflow-y: auto; + padding: 20px; +} + +.pc-table-setting-subtitle { + margin: 0 0 4px; + font-size: 14px; + font-weight: 600; + color: var(--text); +} + +.pc-table-setting-list { + display: flex; + flex-direction: column; + gap: 8px; + padding: 0; + margin: 0; + list-style: none; +} + +.pc-table-setting-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + cursor: grab; +} + +.pc-table-setting-item:active { + cursor: grabbing; +} + +.pc-table-setting-item:hover { + background: rgba(255, 255, 255, 0.06); + border-color: var(--primary); +} + +.pc-table-column-switch .dca-toggle-track { + flex-shrink: 0; +} + +[data-theme="light"] .pc-table-setting-drawer .dca-toggle-thumb { + background: #fff; +} + /* 定投按钮:暗色主题 */ .dca-btn { background: rgba(34, 211, 238, 0.12); diff --git a/app/page.jsx b/app/page.jsx index 670f029..4441235 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -311,6 +311,21 @@ export default function HomePage() { const [refreshMs, setRefreshMs] = useState(60000); const [settingsOpen, setSettingsOpen] = useState(false); const [tempSeconds, setTempSeconds] = useState(60); + const [containerWidth, setContainerWidth] = useState(1200); + + useEffect(() => { + if (typeof window === 'undefined') return; + try { + const raw = window.localStorage.getItem('customSettings'); + if (!raw) return; + const parsed = JSON.parse(raw); + const w = parsed?.pcContainerWidth; + const num = Number(w); + if (Number.isFinite(num)) { + setContainerWidth(Math.min(2000, Math.max(600, num))); + } + } catch { } + }, []); // 全局刷新状态 const [refreshing, setRefreshing] = useState(false); @@ -2539,9 +2554,25 @@ export default function HomePage() { const ms = Math.max(30, Number(tempSeconds)) * 1000; setRefreshMs(ms); storageHelper.setItem('refreshMs', String(ms)); + const w = Math.min(2000, Math.max(600, Number(containerWidth) || 1200)); + setContainerWidth(w); + try { + const raw = window.localStorage.getItem('customSettings'); + const parsed = raw ? JSON.parse(raw) : {}; + window.localStorage.setItem('customSettings', JSON.stringify({ ...parsed, pcContainerWidth: w })); + } catch { } setSettingsOpen(false); }; + const handleResetContainerWidth = () => { + setContainerWidth(1200); + try { + const raw = window.localStorage.getItem('customSettings'); + const parsed = raw ? JSON.parse(raw) : {}; + window.localStorage.setItem('customSettings', JSON.stringify({ ...parsed, pcContainerWidth: 1200 })); + } catch { } + }; + const importFileRef = useRef(null); const [importMsg, setImportMsg] = useState(''); @@ -3301,7 +3332,7 @@ export default function HomePage() { }; return ( -
+
{showThemeTransition && ( )}