* feature_bugfix/251201_vf_h5_ui: fix: 事件详情唔错页面UI调整 fix: 调整事件详情页面 feat: 事件详情页 URL ID 加密,防止用户遍历 style: 首页整体尺寸缩小约 67% fix: 调整客服弹窗 将 PC 端聊天窗口从 380×640 调整为 450×750。 H5 端:宽度占满,高度根据宽度等比缩放 fix: ICP 备案号现在可以点击跳转到 https://beian.miit.gov.cn/ feat: 田间mock数据 feat: 个股中心复用 TradeDatePicker 日期选择器组件 feat: 概念中心历史时间轴弹窗UI调整 feat: 提取日历选择器组件 refactor: 提取 ConceptStocksModal 为通用组件,统一概念中心和个股中心弹窗 refactor: 事件详情弹窗改用 Drawer 组件从底部弹出 fix: 在 viewport meta 标签中添加了 viewport-fit=cover,这样浏览器会将页面内容延伸到曲面屏边缘,同时启用 safe-area-inset-* CSS 环境变量 在普通设备上保持至少 16px 的右侧内边距 在华为 MATE70 PRO 等曲面屏设备上,使用系统提供的安全区域值,避免右侧导航被遮挡 fix: 概念中心H5端卡片尺寸优化,一屏可显示更多内容 fix: 修复自选股添加失败 405 错误 fix: H5端热门事件移除Tooltip避免黑色悬浮框无法消除
206 lines
8.0 KiB
JavaScript
206 lines
8.0 KiB
JavaScript
// src/hooks/useWatchlist.js
|
||
// 自选股管理自定义 Hook(导航栏专用,与 Redux 状态同步)
|
||
|
||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||
import { useSelector, useDispatch } from 'react-redux';
|
||
import { useToast } from '@chakra-ui/react';
|
||
import { logger } from '../utils/logger';
|
||
import { getApiBase } from '../utils/apiConfig';
|
||
import { toggleWatchlist as toggleWatchlistAction, loadWatchlist } from '../store/slices/stockSlice';
|
||
|
||
const WATCHLIST_PAGE_SIZE = 10;
|
||
|
||
/**
|
||
* 自选股管理 Hook(导航栏专用)
|
||
* 提供自选股加载、分页、移除等功能
|
||
* 监听 Redux 中的 watchlist 变化,自动刷新行情数据
|
||
*
|
||
* @returns {{
|
||
* watchlistQuotes: Array,
|
||
* watchlistLoading: boolean,
|
||
* watchlistPage: number,
|
||
* setWatchlistPage: Function,
|
||
* WATCHLIST_PAGE_SIZE: number,
|
||
* loadWatchlistQuotes: Function,
|
||
* followingEvents: Array,
|
||
* handleAddToWatchlist: Function,
|
||
* handleRemoveFromWatchlist: Function,
|
||
* isInWatchlist: Function
|
||
* }}
|
||
*/
|
||
export const useWatchlist = () => {
|
||
const toast = useToast();
|
||
const dispatch = useDispatch();
|
||
const [watchlistQuotes, setWatchlistQuotes] = useState([]);
|
||
const [watchlistLoading, setWatchlistLoading] = useState(false);
|
||
const [watchlistPage, setWatchlistPage] = useState(1);
|
||
const [followingEvents, setFollowingEvents] = useState([]);
|
||
|
||
// 从 Redux 获取自选股列表长度(用于监听变化)
|
||
// 使用 length 作为依赖,避免数组引用变化导致不必要的重新渲染
|
||
const reduxWatchlistLength = useSelector(state => state.stock.watchlist?.length || 0);
|
||
|
||
// 检查 Redux watchlist 是否已初始化(加载状态)
|
||
const reduxWatchlistLoading = useSelector(state => state.stock.loading?.watchlist);
|
||
|
||
// 用于跟踪上一次的 watchlist 长度
|
||
const prevWatchlistLengthRef = useRef(-1); // 初始设为 -1,确保第一次变化也能检测到
|
||
|
||
// 初始化时加载 Redux watchlist(确保 Redux 状态被初始化)
|
||
const hasInitializedRef = useRef(false);
|
||
useEffect(() => {
|
||
if (!hasInitializedRef.current) {
|
||
hasInitializedRef.current = true;
|
||
logger.debug('useWatchlist', '初始化 Redux watchlist');
|
||
dispatch(loadWatchlist());
|
||
}
|
||
}, [dispatch]);
|
||
|
||
// 加载自选股实时行情
|
||
const loadWatchlistQuotes = useCallback(async () => {
|
||
try {
|
||
setWatchlistLoading(true);
|
||
const base = getApiBase();
|
||
const resp = await fetch(base + '/api/account/watchlist/realtime', {
|
||
credentials: 'include',
|
||
cache: 'no-store',
|
||
headers: { 'Cache-Control': 'no-cache' }
|
||
});
|
||
if (resp.ok) {
|
||
const data = await resp.json();
|
||
if (data && data.success && Array.isArray(data.data)) {
|
||
setWatchlistQuotes(data.data);
|
||
logger.debug('useWatchlist', '自选股行情加载成功', { count: data.data.length });
|
||
} else {
|
||
setWatchlistQuotes([]);
|
||
}
|
||
} else {
|
||
setWatchlistQuotes([]);
|
||
}
|
||
} catch (e) {
|
||
logger.warn('useWatchlist', '加载自选股实时行情失败', {
|
||
error: e.message
|
||
});
|
||
setWatchlistQuotes([]);
|
||
} finally {
|
||
setWatchlistLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
// 监听 Redux watchlist 长度变化,自动刷新行情数据
|
||
useEffect(() => {
|
||
const currentLength = reduxWatchlistLength;
|
||
const prevLength = prevWatchlistLengthRef.current;
|
||
|
||
// 只有当 watchlist 长度发生变化时才刷新
|
||
// prevLength = -1 表示初始状态,此时不触发刷新(由菜单打开时触发)
|
||
if (prevLength !== -1 && currentLength !== prevLength) {
|
||
logger.debug('useWatchlist', 'Redux watchlist 长度变化,刷新行情', {
|
||
prevLength,
|
||
currentLength
|
||
});
|
||
|
||
// 延迟一小段时间再刷新,确保后端数据已更新
|
||
const timer = setTimeout(() => {
|
||
logger.debug('useWatchlist', '执行 loadWatchlistQuotes');
|
||
loadWatchlistQuotes();
|
||
}, 500);
|
||
|
||
prevWatchlistLengthRef.current = currentLength;
|
||
return () => clearTimeout(timer);
|
||
}
|
||
|
||
// 更新 ref
|
||
prevWatchlistLengthRef.current = currentLength;
|
||
}, [reduxWatchlistLength, loadWatchlistQuotes]);
|
||
|
||
// 添加到自选股
|
||
const handleAddToWatchlist = useCallback(async (stockCode, stockName) => {
|
||
try {
|
||
const base = getApiBase();
|
||
const resp = await fetch(base + '/api/account/watchlist', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ stock_code: stockCode, stock_name: stockName })
|
||
});
|
||
const data = await resp.json().catch(() => ({}));
|
||
if (resp.ok && data.success) {
|
||
// 刷新自选股列表
|
||
loadWatchlistQuotes();
|
||
toast({ title: '已添加至自选股', status: 'success', duration: 1500 });
|
||
return true;
|
||
} else {
|
||
toast({ title: '添加失败', status: 'error', duration: 2000 });
|
||
return false;
|
||
}
|
||
} catch (e) {
|
||
toast({ title: '网络错误,添加失败', status: 'error', duration: 2000 });
|
||
return false;
|
||
}
|
||
}, [toast, loadWatchlistQuotes]);
|
||
|
||
// 从自选股移除
|
||
const handleRemoveFromWatchlist = useCallback(async (stockCode) => {
|
||
try {
|
||
// 找到股票名称
|
||
const stockItem = watchlistQuotes.find(item => {
|
||
const normalize6 = (code) => {
|
||
const m = String(code || '').match(/(\d{6})/);
|
||
return m ? m[1] : String(code || '');
|
||
};
|
||
return normalize6(item.stock_code) === normalize6(stockCode);
|
||
});
|
||
const stockName = stockItem?.stock_name || '';
|
||
|
||
// 通过 Redux action 移除(会同步更新 Redux 状态)
|
||
await dispatch(toggleWatchlistAction({
|
||
stockCode,
|
||
stockName,
|
||
isInWatchlist: true // 表示当前在自选股中,需要移除
|
||
})).unwrap();
|
||
|
||
// 更新本地状态(立即响应 UI)
|
||
setWatchlistQuotes((prev) => {
|
||
const normalize6 = (code) => {
|
||
const m = String(code || '').match(/(\d{6})/);
|
||
return m ? m[1] : String(code || '');
|
||
};
|
||
const target = normalize6(stockCode);
|
||
const updated = (prev || []).filter((x) => normalize6(x.stock_code) !== target);
|
||
const newMaxPage = Math.max(1, Math.ceil((updated.length || 0) / WATCHLIST_PAGE_SIZE));
|
||
setWatchlistPage((p) => Math.min(p, newMaxPage));
|
||
return updated;
|
||
});
|
||
|
||
toast({ title: '已从自选股移除', status: 'info', duration: 1500 });
|
||
} catch (e) {
|
||
logger.error('useWatchlist', '移除自选股失败', e);
|
||
toast({ title: e.message || '移除失败', status: 'error', duration: 2000 });
|
||
}
|
||
}, [dispatch, watchlistQuotes, toast]);
|
||
|
||
// 判断股票是否在自选股中
|
||
const isInWatchlist = useCallback((stockCode) => {
|
||
const normalize6 = (code) => {
|
||
const m = String(code || '').match(/(\d{6})/);
|
||
return m ? m[1] : String(code || '');
|
||
};
|
||
const target = normalize6(stockCode);
|
||
return watchlistQuotes.some(item => normalize6(item.stock_code) === target);
|
||
}, [watchlistQuotes]);
|
||
|
||
return {
|
||
watchlistQuotes,
|
||
watchlistLoading,
|
||
watchlistPage,
|
||
setWatchlistPage,
|
||
WATCHLIST_PAGE_SIZE,
|
||
loadWatchlistQuotes,
|
||
followingEvents,
|
||
handleAddToWatchlist,
|
||
handleRemoveFromWatchlist,
|
||
isInWatchlist
|
||
};
|
||
};
|