refactor(HomeNavbar): Phase 6 - 提取自选股和关注事件功能组件
Phase 6 重构完成,将自选股和关注事件功能完全组件化: 新增文件: - src/hooks/useWatchlist.js - 自选股管理 Hook (98行) * 管理自选股数据加载、分页和移除逻辑 * 提供 watchlistQuotes、loadWatchlistQuotes、handleRemoveFromWatchlist - src/hooks/useFollowingEvents.js - 关注事件管理 Hook (104行) * 管理关注事件数据加载、分页和取消关注逻辑 * 提供 followingEvents、loadFollowingEvents、handleUnfollowEvent - src/components/Navbars/components/FeatureMenus/WatchlistMenu.js (182行) * 自选股下拉菜单组件,显示实时行情 * 支持分页、价格显示、涨跌幅标记、移除功能 - src/components/Navbars/components/FeatureMenus/FollowingEventsMenu.js (196行) * 关注事件下拉菜单组件,显示事件详情 * 支持分页、事件类型、时间、日均涨幅、周涨幅显示 - src/components/Navbars/components/FeatureMenus/index.js * 统一导出 WatchlistMenu 和 FollowingEventsMenu HomeNavbar.js 优化: - 移除 287 行旧代码(状态定义 + 4个回调函数) - 添加 Phase 6 imports 和 Hook 调用 - 替换自选股菜单 JSX (~77行) → <WatchlistMenu /> - 替换关注事件菜单 JSX (~83行) → <FollowingEventsMenu /> - 812 → 525 行(-287行,-35.3%) Phase 6 成果: - 创建 2 个自定义 Hooks,5 个新文件 - 从 HomeNavbar 中提取 ~450 行复杂逻辑 - 代码更模块化,易于维护和测试 - 所有功能正常,编译通过 总体成果(Phase 1-6): - 原始:1623 行 → 当前:525 行 - 总减少:1098 行(-67.7%) - 提取组件:13+ 个 - 可维护性大幅提升 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
109
src/hooks/useFollowingEvents.js
Normal file
109
src/hooks/useFollowingEvents.js
Normal file
@@ -0,0 +1,109 @@
|
||||
// src/hooks/useFollowingEvents.js
|
||||
// 关注事件管理自定义 Hook
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { logger } from '../utils/logger';
|
||||
import { getApiBase } from '../utils/apiConfig';
|
||||
|
||||
const EVENTS_PAGE_SIZE = 8;
|
||||
|
||||
/**
|
||||
* 关注事件管理 Hook
|
||||
* 提供事件加载、分页、取消关注等功能
|
||||
*
|
||||
* @returns {{
|
||||
* followingEvents: Array,
|
||||
* eventsLoading: boolean,
|
||||
* eventsPage: number,
|
||||
* setEventsPage: Function,
|
||||
* EVENTS_PAGE_SIZE: number,
|
||||
* loadFollowingEvents: Function,
|
||||
* handleUnfollowEvent: Function
|
||||
* }}
|
||||
*/
|
||||
export const useFollowingEvents = () => {
|
||||
const toast = useToast();
|
||||
const [followingEvents, setFollowingEvents] = useState([]);
|
||||
const [eventsLoading, setEventsLoading] = useState(false);
|
||||
const [eventsPage, setEventsPage] = useState(1);
|
||||
|
||||
// 加载关注的事件
|
||||
const loadFollowingEvents = useCallback(async () => {
|
||||
try {
|
||||
setEventsLoading(true);
|
||||
const base = getApiBase();
|
||||
const resp = await fetch(base + '/api/account/events/following', {
|
||||
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)) {
|
||||
// 合并重复的事件(用最新的数据)
|
||||
const eventMap = new Map();
|
||||
for (const evt of data.data) {
|
||||
if (evt && evt.id) {
|
||||
eventMap.set(evt.id, evt);
|
||||
}
|
||||
}
|
||||
const merged = Array.from(eventMap.values());
|
||||
// 按创建时间降序排列(假设事件有 created_at 或 id)
|
||||
if (merged.length > 0 && merged[0].created_at) {
|
||||
merged.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
|
||||
} else {
|
||||
merged.sort((a, b) => (b.id || 0) - (a.id || 0));
|
||||
}
|
||||
setFollowingEvents(merged);
|
||||
} else {
|
||||
setFollowingEvents([]);
|
||||
}
|
||||
} else {
|
||||
setFollowingEvents([]);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('useFollowingEvents', '加载关注事件失败', {
|
||||
error: e.message
|
||||
});
|
||||
setFollowingEvents([]);
|
||||
} finally {
|
||||
setEventsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 取消关注事件
|
||||
const handleUnfollowEvent = useCallback(async (eventId) => {
|
||||
try {
|
||||
const base = getApiBase();
|
||||
const resp = await fetch(base + `/api/events/${eventId}/follow`, {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (resp.ok && data && data.success !== false) {
|
||||
setFollowingEvents((prev) => {
|
||||
const updated = (prev || []).filter((x) => x.id !== eventId);
|
||||
const newMaxPage = Math.max(1, Math.ceil((updated.length || 0) / EVENTS_PAGE_SIZE));
|
||||
setEventsPage((p) => Math.min(p, newMaxPage));
|
||||
return updated;
|
||||
});
|
||||
toast({ title: '已取消关注该事件', status: 'info', duration: 1500 });
|
||||
} else {
|
||||
toast({ title: '操作失败', status: 'error', duration: 2000 });
|
||||
}
|
||||
} catch (e) {
|
||||
toast({ title: '网络错误,操作失败', status: 'error', duration: 2000 });
|
||||
}
|
||||
}, [toast]);
|
||||
|
||||
return {
|
||||
followingEvents,
|
||||
eventsLoading,
|
||||
eventsPage,
|
||||
setEventsPage,
|
||||
EVENTS_PAGE_SIZE,
|
||||
loadFollowingEvents,
|
||||
handleUnfollowEvent
|
||||
};
|
||||
};
|
||||
100
src/hooks/useWatchlist.js
Normal file
100
src/hooks/useWatchlist.js
Normal file
@@ -0,0 +1,100 @@
|
||||
// src/hooks/useWatchlist.js
|
||||
// 自选股管理自定义 Hook
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { logger } from '../utils/logger';
|
||||
import { getApiBase } from '../utils/apiConfig';
|
||||
|
||||
const WATCHLIST_PAGE_SIZE = 10;
|
||||
|
||||
/**
|
||||
* 自选股管理 Hook
|
||||
* 提供自选股加载、分页、移除等功能
|
||||
*
|
||||
* @returns {{
|
||||
* watchlistQuotes: Array,
|
||||
* watchlistLoading: boolean,
|
||||
* watchlistPage: number,
|
||||
* setWatchlistPage: Function,
|
||||
* WATCHLIST_PAGE_SIZE: number,
|
||||
* loadWatchlistQuotes: Function,
|
||||
* handleRemoveFromWatchlist: Function
|
||||
* }}
|
||||
*/
|
||||
export const useWatchlist = () => {
|
||||
const toast = useToast();
|
||||
const [watchlistQuotes, setWatchlistQuotes] = useState([]);
|
||||
const [watchlistLoading, setWatchlistLoading] = useState(false);
|
||||
const [watchlistPage, setWatchlistPage] = useState(1);
|
||||
|
||||
// 加载自选股实时行情
|
||||
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);
|
||||
} else {
|
||||
setWatchlistQuotes([]);
|
||||
}
|
||||
} else {
|
||||
setWatchlistQuotes([]);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('useWatchlist', '加载自选股实时行情失败', {
|
||||
error: e.message
|
||||
});
|
||||
setWatchlistQuotes([]);
|
||||
} finally {
|
||||
setWatchlistLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 从自选股移除
|
||||
const handleRemoveFromWatchlist = useCallback(async (stockCode) => {
|
||||
try {
|
||||
const base = getApiBase();
|
||||
const resp = await fetch(base + `/api/account/watchlist/${stockCode}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (resp.ok && data && data.success !== false) {
|
||||
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 });
|
||||
} else {
|
||||
toast({ title: '移除失败', status: 'error', duration: 2000 });
|
||||
}
|
||||
} catch (e) {
|
||||
toast({ title: '网络错误,移除失败', status: 'error', duration: 2000 });
|
||||
}
|
||||
}, [toast]);
|
||||
|
||||
return {
|
||||
watchlistQuotes,
|
||||
watchlistLoading,
|
||||
watchlistPage,
|
||||
setWatchlistPage,
|
||||
WATCHLIST_PAGE_SIZE,
|
||||
loadWatchlistQuotes,
|
||||
handleRemoveFromWatchlist
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user