feat:个性化数据往云端同步
This commit is contained in:
87
app/components/FitText.jsx
Normal file
87
app/components/FitText.jsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useLayoutEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据容器宽度动态缩小字体,使内容不溢出。
|
||||||
|
* 使用 ResizeObserver 监听容器宽度,内容超出时按比例缩小 fontSize,不低于 minFontSize。
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {React.ReactNode} props.children - 要显示的文本(会单行、不换行)
|
||||||
|
* @param {number} [props.maxFontSize=14] - 最大字号(px)
|
||||||
|
* @param {number} [props.minFontSize=10] - 最小字号(px),再窄也不低于此值
|
||||||
|
* @param {string} [props.className] - 外层容器 className
|
||||||
|
* @param {Object} [props.style] - 外层容器 style(宽度由父级决定,建议父级有明确宽度)
|
||||||
|
* @param {string} [props.as='span'] - 外层容器标签 'span' | 'div'
|
||||||
|
*/
|
||||||
|
export default function FitText({
|
||||||
|
children,
|
||||||
|
maxFontSize = 14,
|
||||||
|
minFontSize = 10,
|
||||||
|
className,
|
||||||
|
style = {},
|
||||||
|
as: Tag = 'span',
|
||||||
|
}) {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const contentRef = useRef(null);
|
||||||
|
|
||||||
|
const adjust = () => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
const content = contentRef.current;
|
||||||
|
if (!container || !content) return;
|
||||||
|
|
||||||
|
const containerWidth = container.clientWidth;
|
||||||
|
if (containerWidth <= 0) return;
|
||||||
|
|
||||||
|
// 先恢复到最大字号再测量,确保在「未缩放」状态下取到真实内容宽度
|
||||||
|
content.style.fontSize = `${maxFontSize}px`;
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
const contentWidth = content.scrollWidth;
|
||||||
|
if (contentWidth <= 0) return;
|
||||||
|
let size = maxFontSize;
|
||||||
|
if (contentWidth > containerWidth) {
|
||||||
|
size = (containerWidth / contentWidth) * maxFontSize;
|
||||||
|
size = Math.max(minFontSize, Math.min(maxFontSize, size));
|
||||||
|
}
|
||||||
|
content.style.fontSize = `${size}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(run);
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
adjust();
|
||||||
|
const ro = new ResizeObserver(adjust);
|
||||||
|
ro.observe(container);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}, [children, maxFontSize, minFontSize]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
ref={containerRef}
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
ref={contentRef}
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
fontWeight: 'inherit',
|
||||||
|
fontSize: `${maxFontSize}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
useSortable,
|
useSortable,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import FitText from './FitText';
|
||||||
import MobileSettingModal from './MobileSettingModal';
|
import MobileSettingModal from './MobileSettingModal';
|
||||||
import { ExitIcon, SettingsIcon, StarIcon } from './Icons';
|
import { ExitIcon, SettingsIcon, StarIcon } from './Icons';
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ export default function MobileFundTable({
|
|||||||
refreshing = false,
|
refreshing = false,
|
||||||
sortBy = 'default',
|
sortBy = 'default',
|
||||||
onReorder,
|
onReorder,
|
||||||
|
onCustomSettingsChange,
|
||||||
}) {
|
}) {
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
@@ -164,6 +166,7 @@ export default function MobileFundTable({
|
|||||||
? { ...parsed, mobileTableColumnOrder: nextOrder }
|
? { ...parsed, mobileTableColumnOrder: nextOrder }
|
||||||
: { mobileTableColumnOrder: nextOrder };
|
: { mobileTableColumnOrder: nextOrder };
|
||||||
window.localStorage.setItem('customSettings', JSON.stringify(nextSettings));
|
window.localStorage.setItem('customSettings', JSON.stringify(nextSettings));
|
||||||
|
onCustomSettingsChange?.();
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
const getStoredMobileColumnVisibility = () => {
|
const getStoredMobileColumnVisibility = () => {
|
||||||
@@ -194,6 +197,7 @@ export default function MobileFundTable({
|
|||||||
? { ...parsed, mobileTableColumnVisibility: nextVisibility }
|
? { ...parsed, mobileTableColumnVisibility: nextVisibility }
|
||||||
: { mobileTableColumnVisibility: nextVisibility };
|
: { mobileTableColumnVisibility: nextVisibility };
|
||||||
window.localStorage.setItem('customSettings', JSON.stringify(nextSettings));
|
window.localStorage.setItem('customSettings', JSON.stringify(nextSettings));
|
||||||
|
onCustomSettingsChange?.();
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -352,7 +356,7 @@ export default function MobileFundTable({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: (info) => <MobileFundNameCell info={info} />,
|
cell: (info) => <MobileFundNameCell info={info} />,
|
||||||
meta: { align: 'left', cellClassName: 'name-cell' },
|
meta: { align: 'left', cellClassName: 'name-cell', width: 140 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'yesterdayChangePercent',
|
accessorKey: 'yesterdayChangePercent',
|
||||||
@@ -367,11 +371,11 @@ export default function MobileFundTable({
|
|||||||
<span className={cls} style={{ fontWeight: 700 }}>
|
<span className={cls} style={{ fontWeight: 700 }}>
|
||||||
{info.getValue() ?? '—'}
|
{info.getValue() ?? '—'}
|
||||||
</span>
|
</span>
|
||||||
<span className="muted" style={{ fontSize: '11px' }}>{date}</span>
|
<span className="muted" style={{ fontSize: '10px' }}>{date}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
meta: { align: 'right', cellClassName: 'change-cell' },
|
meta: { align: 'right', cellClassName: 'change-cell', width: 72 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'estimateChangePercent',
|
accessorKey: 'estimateChangePercent',
|
||||||
@@ -387,11 +391,11 @@ export default function MobileFundTable({
|
|||||||
<span className={cls} style={{ fontWeight: 700 }}>
|
<span className={cls} style={{ fontWeight: 700 }}>
|
||||||
{info.getValue() ?? '—'}
|
{info.getValue() ?? '—'}
|
||||||
</span>
|
</span>
|
||||||
<span className="muted" style={{ fontSize: '11px' }}>{time}</span>
|
<span className="muted" style={{ fontSize: '10px' }}>{time}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
meta: { align: 'right', cellClassName: 'est-change-cell' },
|
meta: { align: 'right', cellClassName: 'est-change-cell', width: 80 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'todayProfit',
|
accessorKey: 'todayProfit',
|
||||||
@@ -402,12 +406,14 @@ export default function MobileFundTable({
|
|||||||
const hasProfit = value != null;
|
const hasProfit = value != null;
|
||||||
const cls = hasProfit ? (value > 0 ? 'up' : value < 0 ? 'down' : '') : 'muted';
|
const cls = hasProfit ? (value > 0 ? 'up' : value < 0 ? 'down' : '') : 'muted';
|
||||||
return (
|
return (
|
||||||
<span className={cls} style={{ fontWeight: 700 }}>
|
<span className={cls} style={{ display: 'block', width: '100%', fontWeight: 700 }}>
|
||||||
|
<FitText maxFontSize={14} minFontSize={10}>
|
||||||
{hasProfit ? (info.getValue() ?? '') : ''}
|
{hasProfit ? (info.getValue() ?? '') : ''}
|
||||||
|
</FitText>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
meta: { align: 'right', cellClassName: 'profit-cell' },
|
meta: { align: 'right', cellClassName: 'profit-cell', width: 80 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'holdingProfit',
|
accessorKey: 'holdingProfit',
|
||||||
@@ -420,20 +426,22 @@ export default function MobileFundTable({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
title="点击切换金额/百分比"
|
title="点击切换金额/百分比"
|
||||||
style={{ cursor: hasTotal ? 'pointer' : 'default' }}
|
style={{ cursor: hasTotal ? 'pointer' : 'default', width: '100%' }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (!hasTotal) return;
|
if (!hasTotal) return;
|
||||||
e.stopPropagation?.();
|
e.stopPropagation?.();
|
||||||
onHoldingProfitClickRef.current?.(original);
|
onHoldingProfitClickRef.current?.(original);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className={cls} style={{ fontWeight: 700 }}>
|
<span className={cls} style={{ display: 'block', width: '100%', fontWeight: 700 }}>
|
||||||
|
<FitText maxFontSize={14} minFontSize={10}>
|
||||||
{hasTotal ? (info.getValue() ?? '') : ''}
|
{hasTotal ? (info.getValue() ?? '') : ''}
|
||||||
|
</FitText>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
meta: { align: 'right', cellClassName: 'holding-cell' },
|
meta: { align: 'right', cellClassName: 'holding-cell', width: 80 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[currentTab, favorites, refreshing]
|
[currentTab, favorites, refreshing]
|
||||||
@@ -474,6 +482,18 @@ export default function MobileFundTable({
|
|||||||
|
|
||||||
const headerGroup = table.getHeaderGroups()[0];
|
const headerGroup = table.getHeaderGroups()[0];
|
||||||
|
|
||||||
|
const LAST_COLUMN_EXTRA = 12;
|
||||||
|
const mobileGridLayout = (() => {
|
||||||
|
if (!headerGroup?.headers?.length) return { gridTemplateColumns: '', minWidth: undefined };
|
||||||
|
const gap = 12;
|
||||||
|
const widths = headerGroup.headers.map((h) => h.column.columnDef.meta?.width ?? 80);
|
||||||
|
if (widths.length > 0) widths[widths.length - 1] += LAST_COLUMN_EXTRA;
|
||||||
|
return {
|
||||||
|
gridTemplateColumns: widths.map((w) => `${w}px`).join(' '),
|
||||||
|
minWidth: widths.reduce((a, b) => a + b, 0) + (widths.length - 1) * gap,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
const getPinClass = (columnId, isHeader) => {
|
const getPinClass = (columnId, isHeader) => {
|
||||||
if (columnId === 'fundName') return isHeader ? 'table-header-cell-pin-left' : 'table-cell-pin-left';
|
if (columnId === 'fundName') return isHeader ? 'table-header-cell-pin-left' : 'table-cell-pin-left';
|
||||||
return '';
|
return '';
|
||||||
@@ -487,17 +507,25 @@ export default function MobileFundTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mobile-fund-table">
|
<div className="mobile-fund-table">
|
||||||
<div className="mobile-fund-table-scroll">
|
<div
|
||||||
|
className="mobile-fund-table-scroll"
|
||||||
|
style={mobileGridLayout.minWidth != null ? { minWidth: mobileGridLayout.minWidth } : undefined}
|
||||||
|
>
|
||||||
{headerGroup && (
|
{headerGroup && (
|
||||||
<div className="table-header-row mobile-fund-table-header">
|
<div
|
||||||
{headerGroup.headers.map((header) => {
|
className="table-header-row mobile-fund-table-header"
|
||||||
|
style={mobileGridLayout.gridTemplateColumns ? { gridTemplateColumns: mobileGridLayout.gridTemplateColumns } : undefined}
|
||||||
|
>
|
||||||
|
{headerGroup.headers.map((header, headerIndex) => {
|
||||||
const columnId = header.column.id;
|
const columnId = header.column.id;
|
||||||
const pinClass = getPinClass(columnId, true);
|
const pinClass = getPinClass(columnId, true);
|
||||||
const alignClass = getAlignClass(columnId);
|
const alignClass = getAlignClass(columnId);
|
||||||
|
const isLastColumn = headerIndex === headerGroup.headers.length - 1;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={header.id}
|
key={header.id}
|
||||||
className={`table-header-cell ${alignClass} ${pinClass}`}
|
className={`table-header-cell ${alignClass} ${pinClass}`}
|
||||||
|
style={isLastColumn ? { paddingRight: LAST_COLUMN_EXTRA } : undefined}
|
||||||
>
|
>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
@@ -536,18 +564,21 @@ export default function MobileFundTable({
|
|||||||
background: 'var(--bg)',
|
background: 'var(--bg)',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
|
...(mobileGridLayout.gridTemplateColumns ? { gridTemplateColumns: mobileGridLayout.gridTemplateColumns } : {}),
|
||||||
}}
|
}}
|
||||||
{...(sortBy === 'default' ? listeners : {})}
|
{...(sortBy === 'default' ? listeners : {})}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => {
|
{row.getVisibleCells().map((cell, cellIndex) => {
|
||||||
const columnId = cell.column.id;
|
const columnId = cell.column.id;
|
||||||
const pinClass = getPinClass(columnId, false);
|
const pinClass = getPinClass(columnId, false);
|
||||||
const alignClass = getAlignClass(columnId);
|
const alignClass = getAlignClass(columnId);
|
||||||
const cellClassName = cell.column.columnDef.meta?.cellClassName || '';
|
const cellClassName = cell.column.columnDef.meta?.cellClassName || '';
|
||||||
|
const isLastColumn = cellIndex === row.getVisibleCells().length - 1;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
className={`table-cell ${alignClass} ${cellClassName} ${pinClass}`}
|
className={`table-cell ${alignClass} ${cellClassName} ${pinClass}`}
|
||||||
|
style={isLastColumn ? { paddingRight: LAST_COLUMN_EXTRA } : undefined}
|
||||||
>
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export default function PcFundTable({
|
|||||||
refreshing = false,
|
refreshing = false,
|
||||||
sortBy = 'default',
|
sortBy = 'default',
|
||||||
onReorder,
|
onReorder,
|
||||||
|
onCustomSettingsChange,
|
||||||
}) {
|
}) {
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
@@ -183,6 +184,7 @@ export default function PcFundTable({
|
|||||||
? { ...parsed, pcTableColumns: nextSizing }
|
? { ...parsed, pcTableColumns: nextSizing }
|
||||||
: { pcTableColumns: nextSizing };
|
: { pcTableColumns: nextSizing };
|
||||||
window.localStorage.setItem('customSettings', JSON.stringify(nextSettings));
|
window.localStorage.setItem('customSettings', JSON.stringify(nextSettings));
|
||||||
|
onCustomSettingsChange?.();
|
||||||
} catch { }
|
} catch { }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,6 +214,7 @@ export default function PcFundTable({
|
|||||||
? { ...parsed, pcTableColumnOrder: nextOrder }
|
? { ...parsed, pcTableColumnOrder: nextOrder }
|
||||||
: { pcTableColumnOrder: nextOrder };
|
: { pcTableColumnOrder: nextOrder };
|
||||||
window.localStorage.setItem('customSettings', JSON.stringify(nextSettings));
|
window.localStorage.setItem('customSettings', JSON.stringify(nextSettings));
|
||||||
|
onCustomSettingsChange?.();
|
||||||
} catch { }
|
} catch { }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -246,6 +249,7 @@ export default function PcFundTable({
|
|||||||
? { ...parsed, pcTableColumnVisibility: nextVisibility }
|
? { ...parsed, pcTableColumnVisibility: nextVisibility }
|
||||||
: { pcTableColumnVisibility: nextVisibility };
|
: { pcTableColumnVisibility: nextVisibility };
|
||||||
window.localStorage.setItem('customSettings', JSON.stringify(nextSettings));
|
window.localStorage.setItem('customSettings', JSON.stringify(nextSettings));
|
||||||
|
onCustomSettingsChange?.();
|
||||||
} catch { }
|
} catch { }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1428,24 +1428,27 @@ input[type="number"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mobile-fund-table-scroll {
|
.mobile-fund-table-scroll {
|
||||||
min-width: 520px;
|
/* min-width 由 MobileFundTable 根据 columns meta.width 动态设置 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-fund-table .table-header-row {
|
.mobile-fund-table .table-header-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 140px 1fr 1fr 1.2fr 1.2fr;
|
/* grid-template-columns 由 MobileFundTable 根据当前列顺序动态设置 */
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
padding-left: 0 !important;
|
padding-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-fund-table .table-header-row .table-header-cell {
|
.mobile-fund-table .table-header-row .table-header-cell {
|
||||||
padding-top: 16px;
|
padding-top: 8px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-fund-table .table-row {
|
.mobile-fund-table .table-row {
|
||||||
grid-template-columns: 140px 1fr 1fr 1.2fr 1.2fr;
|
|
||||||
grid-template-areas: unset;
|
grid-template-areas: unset;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
@@ -1468,6 +1471,9 @@ input[type="number"] {
|
|||||||
.mobile-fund-table .table-row .table-cell {
|
.mobile-fund-table .table-row .table-cell {
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 基金名称列固定左侧:右侧阴影突出固定列,覆盖行 border-bottom */
|
/* 基金名称列固定左侧:右侧阴影突出固定列,覆盖行 border-bottom */
|
||||||
|
|||||||
42
app/page.jsx
42
app/page.jsx
@@ -1502,7 +1502,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
const storageHelper = useMemo(() => {
|
const storageHelper = useMemo(() => {
|
||||||
// 仅以下 key 参与云端同步;fundValuationTimeseries 不同步到云端(测试中功能,暂不同步)
|
// 仅以下 key 参与云端同步;fundValuationTimeseries 不同步到云端(测试中功能,暂不同步)
|
||||||
const keys = new Set(['funds', 'favorites', 'groups', 'collapsedCodes', 'collapsedTrends', 'refreshMs', 'holdings', 'pendingTrades', 'transactions', 'viewMode', 'dcaPlans']);
|
const keys = new Set(['funds', 'favorites', 'groups', 'collapsedCodes', 'collapsedTrends', 'refreshMs', 'holdings', 'pendingTrades', 'transactions', 'viewMode', 'dcaPlans', 'customSettings']);
|
||||||
const triggerSync = (key, prevValue, nextValue) => {
|
const triggerSync = (key, prevValue, nextValue) => {
|
||||||
if (keys.has(key)) {
|
if (keys.has(key)) {
|
||||||
// 标记为脏数据
|
// 标记为脏数据
|
||||||
@@ -1549,7 +1549,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 仅以下 key 的变更会触发云端同步;fundValuationTimeseries 不在其中
|
// 仅以下 key 的变更会触发云端同步;fundValuationTimeseries 不在其中
|
||||||
const keys = new Set(['funds', 'favorites', 'groups', 'collapsedCodes', 'collapsedTrends', 'refreshMs', 'holdings', 'pendingTrades', 'viewMode', 'dcaPlans']);
|
const keys = new Set(['funds', 'favorites', 'groups', 'collapsedCodes', 'collapsedTrends', 'refreshMs', 'holdings', 'pendingTrades', 'viewMode', 'dcaPlans', 'customSettings']);
|
||||||
const onStorage = (e) => {
|
const onStorage = (e) => {
|
||||||
if (!e.key) return;
|
if (!e.key) return;
|
||||||
if (e.key === 'localUpdatedAt') {
|
if (e.key === 'localUpdatedAt') {
|
||||||
@@ -1570,6 +1570,18 @@ export default function HomePage() {
|
|||||||
};
|
};
|
||||||
}, [getFundCodesSignature, scheduleSync]);
|
}, [getFundCodesSignature, scheduleSync]);
|
||||||
|
|
||||||
|
const triggerCustomSettingsSync = useCallback(() => {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
dirtyKeysRef.current.add('customSettings');
|
||||||
|
if (!skipSyncRef.current) {
|
||||||
|
const now = nowInTz().toISOString();
|
||||||
|
window.localStorage.setItem('localUpdatedAt', now);
|
||||||
|
setLastSyncTime(now);
|
||||||
|
}
|
||||||
|
scheduleSync();
|
||||||
|
});
|
||||||
|
}, [scheduleSync]);
|
||||||
|
|
||||||
const applyViewMode = useCallback((mode) => {
|
const applyViewMode = useCallback((mode) => {
|
||||||
if (mode !== 'card' && mode !== 'list') return;
|
if (mode !== 'card' && mode !== 'list') return;
|
||||||
setViewMode(mode);
|
setViewMode(mode);
|
||||||
@@ -2561,6 +2573,7 @@ export default function HomePage() {
|
|||||||
const raw = window.localStorage.getItem('customSettings');
|
const raw = window.localStorage.getItem('customSettings');
|
||||||
const parsed = raw ? JSON.parse(raw) : {};
|
const parsed = raw ? JSON.parse(raw) : {};
|
||||||
window.localStorage.setItem('customSettings', JSON.stringify({ ...parsed, pcContainerWidth: w }));
|
window.localStorage.setItem('customSettings', JSON.stringify({ ...parsed, pcContainerWidth: w }));
|
||||||
|
triggerCustomSettingsSync();
|
||||||
} catch { }
|
} catch { }
|
||||||
setSettingsOpen(false);
|
setSettingsOpen(false);
|
||||||
};
|
};
|
||||||
@@ -2571,6 +2584,7 @@ export default function HomePage() {
|
|||||||
const raw = window.localStorage.getItem('customSettings');
|
const raw = window.localStorage.getItem('customSettings');
|
||||||
const parsed = raw ? JSON.parse(raw) : {};
|
const parsed = raw ? JSON.parse(raw) : {};
|
||||||
window.localStorage.setItem('customSettings', JSON.stringify({ ...parsed, pcContainerWidth: 1200 }));
|
window.localStorage.setItem('customSettings', JSON.stringify({ ...parsed, pcContainerWidth: 1200 }));
|
||||||
|
triggerCustomSettingsSync();
|
||||||
} catch { }
|
} catch { }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2715,6 +2729,7 @@ export default function HomePage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const viewMode = payload.viewMode === 'list' ? 'list' : 'card';
|
const viewMode = payload.viewMode === 'list' ? 'list' : 'card';
|
||||||
|
const customSettings = isPlainObject(payload.customSettings) ? payload.customSettings : {};
|
||||||
|
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
funds: uniqueFundCodes,
|
funds: uniqueFundCodes,
|
||||||
@@ -2727,7 +2742,8 @@ export default function HomePage() {
|
|||||||
pendingTrades,
|
pendingTrades,
|
||||||
transactions,
|
transactions,
|
||||||
dcaPlans,
|
dcaPlans,
|
||||||
viewMode
|
viewMode,
|
||||||
|
customSettings
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2768,6 +2784,13 @@ export default function HomePage() {
|
|||||||
if (!keys || keys.has('dcaPlans')) {
|
if (!keys || keys.has('dcaPlans')) {
|
||||||
all.dcaPlans = JSON.parse(localStorage.getItem('dcaPlans') || '{}');
|
all.dcaPlans = JSON.parse(localStorage.getItem('dcaPlans') || '{}');
|
||||||
}
|
}
|
||||||
|
if (!keys || keys.has('customSettings')) {
|
||||||
|
try {
|
||||||
|
all.customSettings = JSON.parse(localStorage.getItem('customSettings') || '{}');
|
||||||
|
} catch {
|
||||||
|
all.customSettings = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果是全量收集(keys 为 null),进行完整的数据清洗和验证逻辑
|
// 如果是全量收集(keys 为 null),进行完整的数据清洗和验证逻辑
|
||||||
if (!keys) {
|
if (!keys) {
|
||||||
@@ -2837,7 +2860,8 @@ export default function HomePage() {
|
|||||||
pendingTrades: all.pendingTrades,
|
pendingTrades: all.pendingTrades,
|
||||||
transactions: all.transactions,
|
transactions: all.transactions,
|
||||||
dcaPlans: cleanedDcaPlans,
|
dcaPlans: cleanedDcaPlans,
|
||||||
viewMode: all.viewMode
|
viewMode: all.viewMode,
|
||||||
|
customSettings: isPlainObject(all.customSettings) ? all.customSettings : {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2858,6 +2882,7 @@ export default function HomePage() {
|
|||||||
transactions: {},
|
transactions: {},
|
||||||
dcaPlans: {},
|
dcaPlans: {},
|
||||||
viewMode: 'card',
|
viewMode: 'card',
|
||||||
|
customSettings: {},
|
||||||
exportedAt: nowInTz().toISOString()
|
exportedAt: nowInTz().toISOString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2919,6 +2944,13 @@ export default function HomePage() {
|
|||||||
setDcaPlans(nextDcaPlans);
|
setDcaPlans(nextDcaPlans);
|
||||||
storageHelper.setItem('dcaPlans', JSON.stringify(nextDcaPlans));
|
storageHelper.setItem('dcaPlans', JSON.stringify(nextDcaPlans));
|
||||||
|
|
||||||
|
if (isPlainObject(cloudData.customSettings)) {
|
||||||
|
try {
|
||||||
|
const merged = { ...JSON.parse(localStorage.getItem('customSettings') || '{}'), ...cloudData.customSettings };
|
||||||
|
window.localStorage.setItem('customSettings', JSON.stringify(merged));
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
if (nextFunds.length) {
|
if (nextFunds.length) {
|
||||||
const codes = Array.from(new Set(nextFunds.map((f) => f.code)));
|
const codes = Array.from(new Set(nextFunds.map((f) => f.code)));
|
||||||
if (codes.length) await refreshAll(codes);
|
if (codes.length) await refreshAll(codes);
|
||||||
@@ -3947,6 +3979,7 @@ export default function HomePage() {
|
|||||||
if (row.holdingProfitValue == null) return;
|
if (row.holdingProfitValue == null) return;
|
||||||
setPercentModes(prev => ({ ...prev, [row.code]: !prev[row.code] }));
|
setPercentModes(prev => ({ ...prev, [row.code]: !prev[row.code] }));
|
||||||
}}
|
}}
|
||||||
|
onCustomSettingsChange={triggerCustomSettingsSync}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3987,6 +4020,7 @@ export default function HomePage() {
|
|||||||
if (row.holdingProfitValue == null) return;
|
if (row.holdingProfitValue == null) return;
|
||||||
setPercentModes((prev) => ({ ...prev, [row.code]: !prev[row.code] }));
|
setPercentModes((prev) => ({ ...prev, [row.code]: !prev[row.code] }));
|
||||||
}}
|
}}
|
||||||
|
onCustomSettingsChange={triggerCustomSettingsSync}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AnimatePresence mode="popLayout">
|
<AnimatePresence mode="popLayout">
|
||||||
|
|||||||
Reference in New Issue
Block a user