feat: 测试关联板块

This commit is contained in:
hzm
2026-03-17 19:49:33 +08:00
parent b489677d3e
commit fb0dc25341
3 changed files with 171 additions and 5 deletions

View File

@@ -20,6 +20,35 @@ dayjs.tz.setDefault(TZ);
const nowInTz = () => dayjs().tz(TZ);
const toTz = (input) => (input ? dayjs.tz(input, TZ) : nowInTz());
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
/**
* 获取基金「关联板块/跟踪标的」信息(走本地 API并做 1 天缓存)
* 接口:/api/related-sectors?code=xxxxxx
* 返回:{ code: string, relatedSectors: string }
*/
export const fetchRelatedSectors = async (code, { cacheTime = ONE_DAY_MS } = {}) => {
if (!code) return '';
const normalized = String(code).trim();
if (!normalized) return '';
const url = `/api/related-sectors?code=${encodeURIComponent(normalized)}`;
const cacheKey = `relatedSectors:${normalized}`;
try {
const data = await cachedRequest(async () => {
const res = await fetch(url);
if (!res.ok) return null;
return await res.json();
}, cacheKey, { cacheTime });
const relatedSectors = data?.relatedSectors;
return relatedSectors ? String(relatedSectors).trim() : '';
} catch (e) {
return '';
}
};
export const loadScript = (url) => {
if (typeof document === 'undefined' || !document.body) return Promise.resolve(null);

View File

@@ -28,8 +28,10 @@ import FitText from './FitText';
import MobileFundCardDrawer from './MobileFundCardDrawer';
import MobileSettingModal from './MobileSettingModal';
import { DragIcon, ExitIcon, SettingsIcon, SortIcon, StarIcon } from './Icons';
import { fetchRelatedSectors } from '@/app/api/fund';
const MOBILE_NON_FROZEN_COLUMN_IDS = [
'relatedSector',
'yesterdayChangePercent',
'estimateChangePercent',
'totalChangePercent',
@@ -39,6 +41,7 @@ const MOBILE_NON_FROZEN_COLUMN_IDS = [
'estimateNav',
];
const MOBILE_COLUMN_HEADERS = {
relatedSector: '关联板块',
latestNav: '最新净值',
estimateNav: '估算净值',
yesterdayChangePercent: '昨日涨幅',
@@ -233,6 +236,8 @@ export default function MobileFundTable({
const defaultVisibility = (() => {
const o = {};
MOBILE_NON_FROZEN_COLUMN_IDS.forEach((id) => { o[id] = true; });
// 新增列:默认隐藏(用户可在表格设置中开启)
o.relatedSector = false;
return o;
})();
@@ -245,7 +250,11 @@ export default function MobileFundTable({
})();
const mobileColumnVisibility = (() => {
const vis = currentGroupMobile?.mobileTableColumnVisibility ?? null;
if (vis && typeof vis === 'object' && Object.keys(vis).length > 0) return vis;
if (vis && typeof vis === 'object' && Object.keys(vis).length > 0) {
const next = { ...vis };
if (next.relatedSector === undefined) next.relatedSector = false;
return next;
}
return defaultVisibility;
})();
@@ -422,6 +431,7 @@ export default function MobileFundTable({
const LAST_COLUMN_EXTRA = 12;
const FALLBACK_WIDTHS = {
fundName: 140,
relatedSector: 120,
latestNav: 64,
estimateNav: 64,
yesterdayChangePercent: 72,
@@ -431,6 +441,49 @@ export default function MobileFundTable({
holdingProfit: 80,
};
const relatedSectorEnabled = mobileColumnVisibility?.relatedSector !== false;
const relatedSectorCacheRef = useRef(new Map());
const [relatedSectorByCode, setRelatedSectorByCode] = useState({});
const fetchRelatedSector = async (code) => fetchRelatedSectors(code);
const runWithConcurrency = async (items, limit, worker) => {
const queue = [...items];
const runners = Array.from({ length: Math.max(1, limit) }, async () => {
while (queue.length) {
const item = queue.shift();
if (item == null) continue;
// eslint-disable-next-line no-await-in-loop
await worker(item);
}
});
await Promise.all(runners);
};
useEffect(() => {
if (!relatedSectorEnabled) return;
if (!Array.isArray(data) || data.length === 0) return;
const codes = Array.from(new Set(data.map((d) => d?.code).filter(Boolean)));
const missing = codes.filter((code) => !relatedSectorCacheRef.current.has(code));
if (missing.length === 0) return;
let cancelled = false;
(async () => {
await runWithConcurrency(missing, 4, async (code) => {
const value = await fetchRelatedSector(code);
relatedSectorCacheRef.current.set(code, value);
if (cancelled) return;
setRelatedSectorByCode((prev) => {
if (prev[code] === value) return prev;
return { ...prev, [code]: value };
});
});
})();
return () => { cancelled = true; };
}, [relatedSectorEnabled, data]);
const columnWidthMap = useMemo(() => {
const visibleNonNameIds = mobileColumnOrder.filter((id) => mobileColumnVisibility[id] !== false);
const nonNameCount = visibleNonNameIds.length;
@@ -456,6 +509,7 @@ export default function MobileFundTable({
MOBILE_NON_FROZEN_COLUMN_IDS.forEach((id) => {
allVisible[id] = true;
});
allVisible.relatedSector = false;
setMobileColumnVisibility(allVisible);
};
const handleToggleMobileColumnVisibility = (columnId, visible) => {
@@ -654,6 +708,17 @@ export default function MobileFundTable({
),
meta: { align: 'left', cellClassName: 'name-cell', width: columnWidthMap.fundName },
},
{
id: 'relatedSector',
header: '关联板块',
cell: (info) => {
const original = info.row.original || {};
const code = original.code;
const value = (code && (relatedSectorByCode?.[code] ?? relatedSectorCacheRef.current.get(code))) || '';
return value || '—';
},
meta: { align: 'left', cellClassName: 'related-sector-cell', width: columnWidthMap.relatedSector ?? 120 },
},
{
accessorKey: 'latestNav',
header: '最新净值',
@@ -834,7 +899,7 @@ export default function MobileFundTable({
meta: { align: 'right', cellClassName: 'holding-cell', width: columnWidthMap.holdingProfit },
},
],
[currentTab, favorites, refreshing, columnWidthMap, showFullFundName, getFundCardProps, isNameSortMode, sortBy]
[currentTab, favorites, refreshing, columnWidthMap, showFullFundName, getFundCardProps, isNameSortMode, sortBy, relatedSectorByCode]
);
const table = useReactTable({

View File

@@ -34,9 +34,11 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { CloseIcon, DragIcon, ExitIcon, SettingsIcon, StarIcon, TrashIcon, ResetIcon } from './Icons';
import { DragIcon, ExitIcon, SettingsIcon, StarIcon, TrashIcon, ResetIcon } from './Icons';
import { fetchRelatedSectors } from '@/app/api/fund';
const NON_FROZEN_COLUMN_IDS = [
'relatedSector',
'yesterdayChangePercent',
'estimateChangePercent',
'totalChangePercent',
@@ -47,6 +49,7 @@ const NON_FROZEN_COLUMN_IDS = [
'estimateNav',
];
const COLUMN_HEADERS = {
relatedSector: '关联板块',
latestNav: '最新净值',
estimateNav: '估算净值',
yesterdayChangePercent: '昨日涨幅',
@@ -282,9 +285,15 @@ export default function PcFundTable({
})();
const columnVisibility = (() => {
const vis = currentGroupPc?.pcTableColumnVisibility ?? null;
if (vis && typeof vis === 'object' && Object.keys(vis).length > 0) return vis;
if (vis && typeof vis === 'object' && Object.keys(vis).length > 0) {
const next = { ...vis };
if (next.relatedSector === undefined) next.relatedSector = false;
return next;
}
const allVisible = {};
NON_FROZEN_COLUMN_IDS.forEach((id) => { allVisible[id] = true; });
// 新增列:默认隐藏(用户可在表格设置中开启)
allVisible.relatedSector = false;
return allVisible;
})();
const columnSizing = (() => {
@@ -356,6 +365,7 @@ export default function PcFundTable({
NON_FROZEN_COLUMN_IDS.forEach((id) => {
allVisible[id] = true;
});
allVisible.relatedSector = false;
setColumnVisibility(allVisible);
};
const handleToggleColumnVisibility = (columnId, visible) => {
@@ -443,6 +453,51 @@ export default function PcFundTable({
};
}, [stickyTop]);
const relatedSectorEnabled = columnVisibility?.relatedSector !== false;
const relatedSectorCacheRef = useRef(new Map());
const [relatedSectorByCode, setRelatedSectorByCode] = useState({});
const fetchRelatedSector = async (code) => fetchRelatedSectors(code);
const runWithConcurrency = async (items, limit, worker) => {
const queue = [...items];
const results = [];
const runners = Array.from({ length: Math.max(1, limit) }, async () => {
while (queue.length) {
const item = queue.shift();
if (item == null) continue;
// eslint-disable-next-line no-await-in-loop
results.push(await worker(item));
}
});
await Promise.all(runners);
return results;
};
useEffect(() => {
if (!relatedSectorEnabled) return;
if (!Array.isArray(data) || data.length === 0) return;
const codes = Array.from(new Set(data.map((d) => d?.code).filter(Boolean)));
const missing = codes.filter((code) => !relatedSectorCacheRef.current.has(code));
if (missing.length === 0) return;
let cancelled = false;
(async () => {
await runWithConcurrency(missing, 4, async (code) => {
const value = await fetchRelatedSector(code);
relatedSectorCacheRef.current.set(code, value);
if (cancelled) return;
setRelatedSectorByCode((prev) => {
if (prev[code] === value) return prev;
return { ...prev, [code]: value };
});
});
})();
return () => { cancelled = true; };
}, [relatedSectorEnabled, data]);
useEffect(() => {
const tableEl = tableContainerRef.current;
const portalEl = portalHeaderRef.current;
@@ -563,6 +618,22 @@ export default function PcFundTable({
cellClassName: 'name-cell',
},
},
{
id: 'relatedSector',
header: '关联板块',
size: 180,
minSize: 120,
cell: (info) => {
const original = info.row.original || {};
const code = original.code;
const value = (code && (relatedSectorByCode?.[code] ?? relatedSectorCacheRef.current.get(code))) || '';
return value || '—';
},
meta: {
align: 'right',
cellClassName: 'related-sector-cell',
},
},
{
accessorKey: 'latestNav',
header: '最新净值',
@@ -895,7 +966,7 @@ export default function PcFundTable({
},
},
],
[currentTab, favorites, refreshing, sortBy, showFullFundName, getFundCardProps, masked],
[currentTab, favorites, refreshing, sortBy, showFullFundName, getFundCardProps, masked, relatedSectorByCode],
);
const table = useReactTable({
@@ -1125,6 +1196,7 @@ export default function PcFundTable({
const columnId = cell.column.id || cell.column.columnDef?.accessorKey;
const isNameColumn = columnId === 'fundName';
const rightAlignedColumns = new Set([
'relatedSector',
'latestNav',
'estimateNav',
'yesterdayChangePercent',