Compare commits
10 Commits
385d452f5a
...
feature_20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a446f71c04 | ||
|
|
e02cbcd9b7 | ||
|
|
9bb9eab922 | ||
|
|
3d7b0045b7 | ||
|
|
ada9f6e778 | ||
|
|
07aebbece5 | ||
|
|
7a11800cba | ||
|
|
3b352be1a8 | ||
|
|
c49dee72eb | ||
|
|
7159e510a6 |
110
.husky/pre-commit
Executable file
110
.husky/pre-commit
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/bin/sh
|
||||
|
||||
# ============================================
|
||||
# Git Pre-commit Hook
|
||||
# ============================================
|
||||
# 规则:
|
||||
# 1. src 目录下新增的代码文件必须使用 TypeScript (.ts/.tsx)
|
||||
# 2. 修改的代码不能使用 fetch,应使用 axios
|
||||
# ============================================
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
has_error=0
|
||||
|
||||
echo ""
|
||||
echo "🔍 正在检查代码规范..."
|
||||
echo ""
|
||||
|
||||
# ============================================
|
||||
# 规则 1: 新文件必须使用 TypeScript
|
||||
# ============================================
|
||||
|
||||
# 获取新增的文件(只检查 src 目录下的代码文件)
|
||||
new_js_files=$(git diff --cached --name-only --diff-filter=A | grep -E '^src/.*\.(js|jsx)$' || true)
|
||||
|
||||
if [ -n "$new_js_files" ]; then
|
||||
echo "${RED}❌ 错误: 发现新增的 JavaScript 文件${NC}"
|
||||
echo "${YELLOW} 新文件必须使用 TypeScript (.ts/.tsx)${NC}"
|
||||
echo ""
|
||||
echo " 以下文件需要改为 TypeScript:"
|
||||
echo "$new_js_files" | while read file; do
|
||||
echo " - $file"
|
||||
done
|
||||
echo ""
|
||||
echo " 💡 提示: 请将文件扩展名改为 .ts 或 .tsx"
|
||||
echo ""
|
||||
has_error=1
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# 规则 2: 禁止使用 fetch,应使用 axios
|
||||
# ============================================
|
||||
|
||||
# 获取所有暂存的文件(新增 + 修改)
|
||||
staged_files=$(git diff --cached --name-only --diff-filter=AM | grep -E '^src/.*\.(js|jsx|ts|tsx)$' || true)
|
||||
|
||||
if [ -n "$staged_files" ]; then
|
||||
# 检查暂存内容中是否包含 fetch 调用
|
||||
# 使用 git diff --cached 检查实际修改的内容
|
||||
fetch_found=""
|
||||
|
||||
for file in $staged_files; do
|
||||
# 检查该文件暂存的更改中是否有 fetch 调用
|
||||
# 排除注释和字符串中的 fetch
|
||||
# 匹配: fetch(, await fetch, .fetch(
|
||||
fetch_matches=$(git diff --cached -U0 "$file" 2>/dev/null | grep -E '^\+.*[^a-zA-Z_]fetch\s*\(' | grep -v '^\+\s*//' || true)
|
||||
|
||||
if [ -n "$fetch_matches" ]; then
|
||||
fetch_found="$fetch_found
|
||||
$file"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$fetch_found" ]; then
|
||||
echo "${RED}❌ 错误: 检测到使用了 fetch API${NC}"
|
||||
echo "${YELLOW} 请使用 axios 进行 HTTP 请求${NC}"
|
||||
echo ""
|
||||
echo " 以下文件包含 fetch 调用:"
|
||||
echo "$fetch_found" | while read file; do
|
||||
if [ -n "$file" ]; then
|
||||
echo " - $file"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo " 💡 修改建议:"
|
||||
echo " ${GREEN}// 替换前${NC}"
|
||||
echo " fetch('/api/data').then(res => res.json())"
|
||||
echo ""
|
||||
echo " ${GREEN}// 替换后${NC}"
|
||||
echo " import axios from 'axios';"
|
||||
echo " axios.get('/api/data').then(res => res.data)"
|
||||
echo ""
|
||||
has_error=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# 检查结果
|
||||
# ============================================
|
||||
|
||||
if [ $has_error -eq 1 ]; then
|
||||
echo "${RED}========================================${NC}"
|
||||
echo "${RED}提交被阻止,请修复以上问题后重试${NC}"
|
||||
echo "${RED}========================================${NC}"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${GREEN}✅ 代码规范检查通过${NC}"
|
||||
echo ""
|
||||
|
||||
# 运行 lint-staged(如果配置了)
|
||||
# 可选:在 package.json 中添加 "lint-staged" 配置来启用代码格式化
|
||||
# if [ -f "package.json" ] && grep -q '"lint-staged"' package.json; then
|
||||
# npx lint-staged
|
||||
# fi
|
||||
@@ -131,12 +131,14 @@
|
||||
"eslint-plugin-prettier": "3.4.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-append-prepend": "1.0.9",
|
||||
"husky": "^9.1.7",
|
||||
"imagemin": "^9.0.1",
|
||||
"imagemin-mozjpeg": "^10.0.0",
|
||||
"imagemin-pngquant": "^10.0.0",
|
||||
"kill-port": "^2.0.1",
|
||||
"less": "^4.4.2",
|
||||
"less-loader": "^12.3.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"msw": "^2.11.5",
|
||||
"prettier": "2.2.1",
|
||||
"react-error-overlay": "6.0.9",
|
||||
|
||||
@@ -118,6 +118,11 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
|
||||
// 当前索引
|
||||
const currentIndex = controlledIndex ?? internalIndex;
|
||||
|
||||
// 记录已访问的 Tab 索引(用于真正的懒加载)
|
||||
const [visitedTabs, setVisitedTabs] = useState<Set<number>>(
|
||||
() => new Set([controlledIndex ?? defaultIndex])
|
||||
);
|
||||
|
||||
// 合并主题
|
||||
const theme: SubTabTheme = {
|
||||
...THEME_PRESETS[themePreset],
|
||||
@@ -132,6 +137,12 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
|
||||
const tabKey = tabs[newIndex]?.key || '';
|
||||
onTabChange?.(newIndex, tabKey);
|
||||
|
||||
// 记录已访问的 Tab(用于懒加载)
|
||||
setVisitedTabs(prev => {
|
||||
if (prev.has(newIndex)) return prev;
|
||||
return new Set(prev).add(newIndex);
|
||||
});
|
||||
|
||||
if (controlledIndex === undefined) {
|
||||
setInternalIndex(newIndex);
|
||||
}
|
||||
@@ -197,11 +208,16 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
|
||||
</TabList>
|
||||
|
||||
<TabPanels p={contentPadding}>
|
||||
{tabs.map((tab) => {
|
||||
{tabs.map((tab, idx) => {
|
||||
const Component = tab.component;
|
||||
// 懒加载:只渲染已访问过的 Tab
|
||||
const shouldRender = !isLazy || visitedTabs.has(idx);
|
||||
|
||||
return (
|
||||
<TabPanel key={tab.key} p={0}>
|
||||
{Component ? <Component {...componentProps} /> : null}
|
||||
{shouldRender && Component ? (
|
||||
<Component {...componentProps} />
|
||||
) : null}
|
||||
</TabPanel>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -661,6 +661,12 @@ export const NotificationProvider = ({ children }) => {
|
||||
|
||||
// ========== 连接到 Socket 服务(⚡ 异步初始化,不阻塞首屏) ==========
|
||||
useEffect(() => {
|
||||
// ⚡ Mock 模式下跳过 Socket 连接(避免连接生产服务器失败的错误)
|
||||
if (process.env.REACT_APP_ENABLE_MOCK === 'true') {
|
||||
logger.debug('NotificationContext', 'Mock 模式,跳过 Socket 连接');
|
||||
return;
|
||||
}
|
||||
|
||||
// ⚡ 防止 React Strict Mode 导致的重复初始化
|
||||
if (socketInitialized) {
|
||||
logger.debug('NotificationContext', 'Socket 已初始化,跳过重复执行(Strict Mode 保护)');
|
||||
|
||||
11
src/index.js
11
src/index.js
@@ -5,6 +5,17 @@ import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
// ⚡ 性能监控:在应用启动时尽早标记
|
||||
import { performanceMonitor } from './utils/performanceMonitor';
|
||||
|
||||
// T0: HTML 加载完成时间点
|
||||
if (document.readyState === 'complete') {
|
||||
performanceMonitor.mark('html-loaded');
|
||||
} else {
|
||||
window.addEventListener('load', () => {
|
||||
performanceMonitor.mark('html-loaded');
|
||||
});
|
||||
}
|
||||
|
||||
// T1: React 开始初始化
|
||||
performanceMonitor.mark('app-start');
|
||||
|
||||
// ⚡ 已删除 brainwave.css(项目未安装 Tailwind CSS,该文件无效)
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
// src/mocks/handlers/bytedesk.js
|
||||
/**
|
||||
* Bytedesk 客服 Widget MSW Handler
|
||||
* 使用 passthrough 让请求通过到真实服务器,消除 MSW 警告
|
||||
* Mock 模式下返回模拟数据
|
||||
*/
|
||||
|
||||
import { http, passthrough } from 'msw';
|
||||
import { http, HttpResponse, passthrough } from 'msw';
|
||||
|
||||
export const bytedeskHandlers = [
|
||||
// Bytedesk API 请求 - 直接 passthrough
|
||||
// 匹配 /bytedesk/* 路径(通过代理访问后端)
|
||||
// 未读消息数量
|
||||
http.get('/bytedesk/visitor/api/v1/message/unread/count', () => {
|
||||
return HttpResponse.json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: { count: 0 },
|
||||
});
|
||||
}),
|
||||
|
||||
// 其他 Bytedesk API - 返回通用成功响应
|
||||
http.all('/bytedesk/*', () => {
|
||||
return passthrough();
|
||||
return HttpResponse.json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: null,
|
||||
});
|
||||
}),
|
||||
|
||||
// Bytedesk 外部 CDN/服务请求
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 性能监控工具 - 统计白屏时间和性能指标
|
||||
|
||||
import { logger } from './logger';
|
||||
import { reportPerformanceMetrics } from '../lib/posthog';
|
||||
|
||||
/**
|
||||
* 性能指标接口
|
||||
@@ -208,6 +209,9 @@ class PerformanceMonitor {
|
||||
// 性能分析建议
|
||||
this.analyzePerformance();
|
||||
|
||||
// 上报性能指标到 PostHog
|
||||
reportPerformanceMetrics(this.metrics);
|
||||
|
||||
return this.metrics;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Company 目录结构说明
|
||||
|
||||
> 最后更新:2025-12-17(StockQuoteCard 数据下沉优化)
|
||||
> 最后更新:2025-12-17(API 接口清单梳理)
|
||||
|
||||
## 目录结构
|
||||
|
||||
@@ -230,6 +230,83 @@ src/views/Company/
|
||||
|
||||
---
|
||||
|
||||
## API 接口清单
|
||||
|
||||
Company 模块共使用 **27 个** API 接口(去重后)。
|
||||
|
||||
### 一、股票基础信息 (8 个)
|
||||
|
||||
| 接口 | 方法 | 调用位置 |
|
||||
|------|------|----------|
|
||||
| `/api/stock/${stockCode}/basic-info` | GET | useBasicInfo.ts, useStockQuoteData.ts, NewsPanel.js |
|
||||
| `/api/stock/${stockCode}/branches` | GET | useBranchesData.ts |
|
||||
| `/api/stock/${stockCode}/management?active_only=true` | GET | useManagementData.ts |
|
||||
| `/api/stock/${stockCode}/announcements?limit=20` | GET | useAnnouncementsData.ts |
|
||||
| `/api/stock/${stockCode}/disclosure-schedule` | GET | useDisclosureData.ts |
|
||||
| `/api/stock/${stockCode}/forecast` | GET | ForecastPanel.js |
|
||||
| `/api/stock/${stockCode}/forecast-report` | GET | ForecastReport/index.tsx |
|
||||
| `/api/stock/${stockCode}/latest-minute` | GET | marketService.ts |
|
||||
|
||||
### 二、股东信息 (4 个)
|
||||
|
||||
| 接口 | 方法 | 调用位置 |
|
||||
|------|------|----------|
|
||||
| `/api/stock/${stockCode}/actual-control` | GET | useShareholderData.ts |
|
||||
| `/api/stock/${stockCode}/concentration` | GET | useShareholderData.ts |
|
||||
| `/api/stock/${stockCode}/top-shareholders?limit=10` | GET | useShareholderData.ts |
|
||||
| `/api/stock/${stockCode}/top-circulation-shareholders?limit=10` | GET | useShareholderData.ts |
|
||||
|
||||
### 三、行情数据 (8 个)
|
||||
|
||||
| 接口 | 方法 | 调用位置 |
|
||||
|------|------|----------|
|
||||
| `/api/stock/quotes` | POST | stockService.getQuotes |
|
||||
| `/api/market/summary/${stockCode}` | GET | marketService.ts |
|
||||
| `/api/market/trade/${stockCode}?days=${days}` | GET | marketService.ts |
|
||||
| `/api/market/funding/${stockCode}?days=${days}` | GET | marketService.ts |
|
||||
| `/api/market/bigdeal/${stockCode}?days=${days}` | GET | marketService.ts |
|
||||
| `/api/market/unusual/${stockCode}?days=${days}` | GET | marketService.ts |
|
||||
| `/api/market/pledge/${stockCode}` | GET | marketService.ts |
|
||||
| `/api/market/rise-analysis/${stockCode}` | GET | marketService.ts |
|
||||
|
||||
### 四、深度分析 (5 个)
|
||||
|
||||
| 接口 | 方法 | 调用位置 |
|
||||
|------|------|----------|
|
||||
| `/api/company/comprehensive-analysis/${stockCode}` | GET | DeepAnalysis/index.js |
|
||||
| `/api/company/value-chain-analysis/${stockCode}` | GET | DeepAnalysis/index.js |
|
||||
| `/api/company/key-factors-timeline/${stockCode}` | GET | DeepAnalysis/index.js |
|
||||
| `/api/company/value-chain/related-companies?node_name=...` | GET | ValueChainNodeCard/index.tsx |
|
||||
| `/api/financial/industry-rank/${stockCode}` | GET | DeepAnalysis/index.js |
|
||||
|
||||
### 五、财务数据 (1 个)
|
||||
|
||||
| 接口 | 方法 | 调用位置 |
|
||||
|------|------|----------|
|
||||
| `/api/financial/financial-metrics/${stockCode}?limit=${limit}` | GET | financialService.getFinancialMetrics |
|
||||
|
||||
### 六、事件/新闻 (1 个)
|
||||
|
||||
| 接口 | 方法 | 调用位置 |
|
||||
|------|------|----------|
|
||||
| `/api/events?q=${searchTerm}&page=${page}&per_page=10` | GET | NewsPanel.js |
|
||||
|
||||
### 统计汇总
|
||||
|
||||
| 分类 | 数量 |
|
||||
|------|------|
|
||||
| 股票基础信息 | 8 |
|
||||
| 股东信息 | 4 |
|
||||
| 行情数据 | 8 |
|
||||
| 深度分析 | 5 |
|
||||
| 财务数据 | 1 |
|
||||
| 事件/新闻 | 1 |
|
||||
| **去重后总计** | **27** |
|
||||
|
||||
> 注:`/api/stock/${stockCode}/basic-info` 在 3 处调用,但只算 1 个接口。
|
||||
|
||||
---
|
||||
|
||||
## 文件职责说明
|
||||
|
||||
### 入口文件
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { SearchIcon } from '@chakra-ui/icons';
|
||||
import { useStockSearch } from '../../hooks/useStockSearch';
|
||||
|
||||
/**
|
||||
* 股票搜索栏组件(带模糊搜索下拉)
|
||||
@@ -31,27 +32,18 @@ const SearchBar = ({
|
||||
}) => {
|
||||
// 下拉状态
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [filteredStocks, setFilteredStocks] = useState([]);
|
||||
const containerRef = useRef(null);
|
||||
|
||||
// 从 Redux 获取全部股票列表
|
||||
const allStocks = useSelector(state => state.stock.allStocks);
|
||||
|
||||
// 模糊搜索过滤
|
||||
// 使用共享的搜索 Hook
|
||||
const filteredStocks = useStockSearch(allStocks, inputCode, { limit: 10 });
|
||||
|
||||
// 根据搜索结果更新下拉显示状态
|
||||
useEffect(() => {
|
||||
if (inputCode && inputCode.trim()) {
|
||||
const searchTerm = inputCode.trim().toLowerCase();
|
||||
const filtered = allStocks.filter(stock =>
|
||||
stock.code.toLowerCase().includes(searchTerm) ||
|
||||
stock.name.includes(inputCode.trim())
|
||||
).slice(0, 10); // 限制显示10条
|
||||
setFilteredStocks(filtered);
|
||||
setShowDropdown(filtered.length > 0);
|
||||
} else {
|
||||
setFilteredStocks([]);
|
||||
setShowDropdown(false);
|
||||
}
|
||||
}, [inputCode, allStocks]);
|
||||
setShowDropdown(filteredStocks.length > 0 && !!inputCode?.trim());
|
||||
}, [filteredStocks, inputCode]);
|
||||
|
||||
// 点击外部关闭下拉
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/CompanyOverview/hooks/useAnnouncementsData.ts
|
||||
// 公告数据 Hook - 用于公司公告 Tab
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
import type { Announcement } from "../types";
|
||||
@@ -26,33 +26,38 @@ export const useAnnouncementsData = (stockCode?: string): UseAnnouncementsDataRe
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<Announcement[]>>(
|
||||
`/api/stock/${stockCode}/announcements?limit=20`
|
||||
);
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (result.success) {
|
||||
setAnnouncements(result.data);
|
||||
} else {
|
||||
setError("加载公告数据失败");
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<Announcement[]>>(
|
||||
`/api/stock/${stockCode}/announcements?limit=20`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setAnnouncements(result.data);
|
||||
} else {
|
||||
setError("加载公告数据失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === "CanceledError") return;
|
||||
logger.error("useAnnouncementsData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("useAnnouncementsData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
return () => controller.abort();
|
||||
}, [stockCode]);
|
||||
|
||||
return { announcements, loading, error };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/CompanyOverview/hooks/useBasicInfo.ts
|
||||
// 公司基本信息 Hook - 用于 CompanyHeaderCard
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
import type { BasicInfo } from "../types";
|
||||
@@ -26,33 +26,38 @@ export const useBasicInfo = (stockCode?: string): UseBasicInfoResult => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<BasicInfo>>(
|
||||
`/api/stock/${stockCode}/basic-info`
|
||||
);
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (result.success) {
|
||||
setBasicInfo(result.data);
|
||||
} else {
|
||||
setError("加载基本信息失败");
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<BasicInfo>>(
|
||||
`/api/stock/${stockCode}/basic-info`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setBasicInfo(result.data);
|
||||
} else {
|
||||
setError("加载基本信息失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === "CanceledError") return;
|
||||
logger.error("useBasicInfo", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("useBasicInfo", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
return () => controller.abort();
|
||||
}, [stockCode]);
|
||||
|
||||
return { basicInfo, loading, error };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/CompanyOverview/hooks/useBranchesData.ts
|
||||
// 分支机构数据 Hook - 用于分支机构 Tab
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
import type { Branch } from "../types";
|
||||
@@ -26,33 +26,38 @@ export const useBranchesData = (stockCode?: string): UseBranchesDataResult => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<Branch[]>>(
|
||||
`/api/stock/${stockCode}/branches`
|
||||
);
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (result.success) {
|
||||
setBranches(result.data);
|
||||
} else {
|
||||
setError("加载分支机构数据失败");
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<Branch[]>>(
|
||||
`/api/stock/${stockCode}/branches`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setBranches(result.data);
|
||||
} else {
|
||||
setError("加载分支机构数据失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === "CanceledError") return;
|
||||
logger.error("useBranchesData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("useBranchesData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
return () => controller.abort();
|
||||
}, [stockCode]);
|
||||
|
||||
return { branches, loading, error };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/CompanyOverview/hooks/useDisclosureData.ts
|
||||
// 披露日程数据 Hook - 用于工商信息 Tab
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
import type { DisclosureSchedule } from "../types";
|
||||
@@ -26,33 +26,38 @@ export const useDisclosureData = (stockCode?: string): UseDisclosureDataResult =
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<DisclosureSchedule[]>>(
|
||||
`/api/stock/${stockCode}/disclosure-schedule`
|
||||
);
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (result.success) {
|
||||
setDisclosureSchedule(result.data);
|
||||
} else {
|
||||
setError("加载披露日程数据失败");
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<DisclosureSchedule[]>>(
|
||||
`/api/stock/${stockCode}/disclosure-schedule`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setDisclosureSchedule(result.data);
|
||||
} else {
|
||||
setError("加载披露日程数据失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === "CanceledError") return;
|
||||
logger.error("useDisclosureData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("useDisclosureData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
return () => controller.abort();
|
||||
}, [stockCode]);
|
||||
|
||||
return { disclosureSchedule, loading, error };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/CompanyOverview/hooks/useManagementData.ts
|
||||
// 管理团队数据 Hook - 用于管理团队 Tab
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
import type { Management } from "../types";
|
||||
@@ -26,33 +26,38 @@ export const useManagementData = (stockCode?: string): UseManagementDataResult =
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<Management[]>>(
|
||||
`/api/stock/${stockCode}/management?active_only=true`
|
||||
);
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (result.success) {
|
||||
setManagement(result.data);
|
||||
} else {
|
||||
setError("加载管理团队数据失败");
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<Management[]>>(
|
||||
`/api/stock/${stockCode}/management?active_only=true`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setManagement(result.data);
|
||||
} else {
|
||||
setError("加载管理团队数据失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === "CanceledError") return;
|
||||
logger.error("useManagementData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("useManagementData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
return () => controller.abort();
|
||||
}, [stockCode]);
|
||||
|
||||
return { management, loading, error };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/CompanyOverview/hooks/useShareholderData.ts
|
||||
// 股权结构数据 Hook - 用于股权结构 Tab
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
import type { ActualControl, Concentration, Shareholder } from "../types";
|
||||
@@ -32,40 +32,44 @@ export const useShareholderData = (stockCode?: string): UseShareholderDataResult
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const [
|
||||
{ data: actualRes },
|
||||
{ data: concentrationRes },
|
||||
{ data: shareholdersRes },
|
||||
{ data: circulationRes },
|
||||
] = await Promise.all([
|
||||
axios.get<ApiResponse<ActualControl[]>>(`/api/stock/${stockCode}/actual-control`),
|
||||
axios.get<ApiResponse<Concentration[]>>(`/api/stock/${stockCode}/concentration`),
|
||||
axios.get<ApiResponse<Shareholder[]>>(`/api/stock/${stockCode}/top-shareholders?limit=10`),
|
||||
axios.get<ApiResponse<Shareholder[]>>(`/api/stock/${stockCode}/top-circulation-shareholders?limit=10`),
|
||||
]);
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (actualRes.success) setActualControl(actualRes.data);
|
||||
if (concentrationRes.success) setConcentration(concentrationRes.data);
|
||||
if (shareholdersRes.success) setTopShareholders(shareholdersRes.data);
|
||||
if (circulationRes.success) setTopCirculationShareholders(circulationRes.data);
|
||||
} catch (err) {
|
||||
logger.error("useShareholderData", "loadData", err, { stockCode });
|
||||
setError("加载股权结构数据失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
try {
|
||||
const [
|
||||
{ data: actualRes },
|
||||
{ data: concentrationRes },
|
||||
{ data: shareholdersRes },
|
||||
{ data: circulationRes },
|
||||
] = await Promise.all([
|
||||
axios.get<ApiResponse<ActualControl[]>>(`/api/stock/${stockCode}/actual-control`, { signal: controller.signal }),
|
||||
axios.get<ApiResponse<Concentration[]>>(`/api/stock/${stockCode}/concentration`, { signal: controller.signal }),
|
||||
axios.get<ApiResponse<Shareholder[]>>(`/api/stock/${stockCode}/top-shareholders?limit=10`, { signal: controller.signal }),
|
||||
axios.get<ApiResponse<Shareholder[]>>(`/api/stock/${stockCode}/top-circulation-shareholders?limit=10`, { signal: controller.signal }),
|
||||
]);
|
||||
|
||||
if (actualRes.success) setActualControl(actualRes.data);
|
||||
if (concentrationRes.success) setConcentration(concentrationRes.data);
|
||||
if (shareholdersRes.success) setTopShareholders(shareholdersRes.data);
|
||||
if (circulationRes.success) setTopCirculationShareholders(circulationRes.data);
|
||||
} catch (err: any) {
|
||||
if (err.name === "CanceledError") return;
|
||||
logger.error("useShareholderData", "loadData", err, { stockCode });
|
||||
setError("加载股权结构数据失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
return () => controller.abort();
|
||||
}, [stockCode]);
|
||||
|
||||
return {
|
||||
actualControl,
|
||||
|
||||
@@ -23,19 +23,6 @@ interface ApiResponse<T> {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 API 请求函数
|
||||
*/
|
||||
const apiRequest = async <T>(url: string): Promise<ApiResponse<T>> => {
|
||||
try {
|
||||
const { data } = await axios.get<ApiResponse<T>>(url);
|
||||
return data;
|
||||
} catch (error) {
|
||||
logger.error('marketService', 'apiRequest', error, { url });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 市场数据服务
|
||||
*/
|
||||
@@ -45,7 +32,8 @@ export const marketService = {
|
||||
* @param stockCode 股票代码
|
||||
*/
|
||||
async getMarketSummary(stockCode: string): Promise<ApiResponse<MarketSummary>> {
|
||||
return apiRequest<MarketSummary>(`/api/market/summary/${stockCode}`);
|
||||
const { data } = await axios.get<ApiResponse<MarketSummary>>(`/api/market/summary/${stockCode}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -54,7 +42,8 @@ export const marketService = {
|
||||
* @param days 天数,默认 60 天
|
||||
*/
|
||||
async getTradeData(stockCode: string, days: number = 60): Promise<ApiResponse<TradeDayData[]>> {
|
||||
return apiRequest<TradeDayData[]>(`/api/market/trade/${stockCode}?days=${days}`);
|
||||
const { data } = await axios.get<ApiResponse<TradeDayData[]>>(`/api/market/trade/${stockCode}?days=${days}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -63,7 +52,8 @@ export const marketService = {
|
||||
* @param days 天数,默认 30 天
|
||||
*/
|
||||
async getFundingData(stockCode: string, days: number = 30): Promise<ApiResponse<FundingDayData[]>> {
|
||||
return apiRequest<FundingDayData[]>(`/api/market/funding/${stockCode}?days=${days}`);
|
||||
const { data } = await axios.get<ApiResponse<FundingDayData[]>>(`/api/market/funding/${stockCode}?days=${days}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -91,7 +81,8 @@ export const marketService = {
|
||||
* @param stockCode 股票代码
|
||||
*/
|
||||
async getPledgeData(stockCode: string): Promise<ApiResponse<PledgeData[]>> {
|
||||
return apiRequest<PledgeData[]>(`/api/market/pledge/${stockCode}`);
|
||||
const { data } = await axios.get<ApiResponse<PledgeData[]>>(`/api/market/pledge/${stockCode}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -109,7 +100,8 @@ export const marketService = {
|
||||
if (startDate && endDate) {
|
||||
url += `?start_date=${startDate}&end_date=${endDate}`;
|
||||
}
|
||||
return apiRequest<RiseAnalysis[]>(url);
|
||||
const { data } = await axios.get<ApiResponse<RiseAnalysis[]>>(url);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { SearchIcon } from '@chakra-ui/icons';
|
||||
import { BarChart2 } from 'lucide-react';
|
||||
import { useStockSearch, type Stock } from '../../../hooks/useStockSearch';
|
||||
|
||||
interface CompareStockInputProps {
|
||||
onCompare: (stockCode: string) => void;
|
||||
@@ -25,11 +26,6 @@ interface CompareStockInputProps {
|
||||
currentStockCode?: string;
|
||||
}
|
||||
|
||||
interface Stock {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface RootState {
|
||||
stock: {
|
||||
allStocks: Stock[];
|
||||
@@ -43,7 +39,6 @@ const CompareStockInput: React.FC<CompareStockInputProps> = ({
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [filteredStocks, setFilteredStocks] = useState<Stock[]>([]);
|
||||
const [selectedStock, setSelectedStock] = useState<Stock | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -55,25 +50,16 @@ const CompareStockInput: React.FC<CompareStockInputProps> = ({
|
||||
const goldColor = '#F4D03F';
|
||||
const bgColor = '#1A202C';
|
||||
|
||||
// 模糊搜索过滤
|
||||
// 使用共享的搜索 Hook(排除当前股票)
|
||||
const filteredStocks = useStockSearch(allStocks, inputValue, {
|
||||
excludeCode: currentStockCode,
|
||||
limit: 8,
|
||||
});
|
||||
|
||||
// 根据搜索结果更新下拉显示状态
|
||||
useEffect(() => {
|
||||
if (inputValue && inputValue.trim()) {
|
||||
const searchTerm = inputValue.trim().toLowerCase();
|
||||
const filtered = allStocks
|
||||
.filter(
|
||||
(stock) =>
|
||||
stock.code !== currentStockCode && // 排除当前股票
|
||||
(stock.code.toLowerCase().includes(searchTerm) ||
|
||||
stock.name.includes(inputValue.trim()))
|
||||
)
|
||||
.slice(0, 8); // 限制显示8条
|
||||
setFilteredStocks(filtered);
|
||||
setShowDropdown(filtered.length > 0);
|
||||
} else {
|
||||
setFilteredStocks([]);
|
||||
setShowDropdown(false);
|
||||
}
|
||||
}, [inputValue, allStocks, currentStockCode]);
|
||||
setShowDropdown(filteredStocks.length > 0 && !!inputValue?.trim());
|
||||
}, [filteredStocks, inputValue]);
|
||||
|
||||
// 点击外部关闭下拉
|
||||
useEffect(() => {
|
||||
|
||||
@@ -73,24 +73,18 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
|
||||
const [basicLoading, setBasicLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 获取行情数据
|
||||
const fetchQuote = useCallback(async () => {
|
||||
if (!stockCode) {
|
||||
setQuoteData(null);
|
||||
return;
|
||||
}
|
||||
// 用于手动刷新的 ref
|
||||
const refetchRef = useCallback(async () => {
|
||||
if (!stockCode) return;
|
||||
|
||||
// 获取行情数据
|
||||
setQuoteLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
logger.debug('useStockQuoteData', '获取股票行情', { stockCode });
|
||||
const quotes = await stockService.getQuotes([stockCode]);
|
||||
|
||||
// API 返回格式: { [stockCode]: quoteData }
|
||||
const quoteResult = quotes?.[stockCode] || quotes;
|
||||
const transformedData = transformQuoteData(quoteResult, stockCode);
|
||||
|
||||
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
|
||||
setQuoteData(transformedData);
|
||||
} catch (err) {
|
||||
@@ -100,49 +94,85 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
|
||||
} finally {
|
||||
setQuoteLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
|
||||
// 获取基本信息
|
||||
const fetchBasicInfo = useCallback(async () => {
|
||||
if (!stockCode) {
|
||||
setBasicInfo(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取基本信息
|
||||
setBasicLoading(true);
|
||||
|
||||
try {
|
||||
const { data: result } = await axios.get(`/api/stock/${stockCode}/basic-info`);
|
||||
|
||||
if (result.success) {
|
||||
setBasicInfo(result.data);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('useStockQuoteData', '获取基本信息失败', err);
|
||||
// 基本信息获取失败不影响主流程,只记录日志
|
||||
} finally {
|
||||
setBasicLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
|
||||
// stockCode 变化时重新获取数据
|
||||
// stockCode 变化时重新获取数据(带取消支持)
|
||||
useEffect(() => {
|
||||
fetchQuote();
|
||||
fetchBasicInfo();
|
||||
}, [fetchQuote, fetchBasicInfo]);
|
||||
if (!stockCode) {
|
||||
setQuoteData(null);
|
||||
setBasicInfo(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 手动刷新
|
||||
const refetch = useCallback(() => {
|
||||
fetchQuote();
|
||||
fetchBasicInfo();
|
||||
}, [fetchQuote, fetchBasicInfo]);
|
||||
const controller = new AbortController();
|
||||
let isCancelled = false;
|
||||
|
||||
const fetchData = async () => {
|
||||
// 获取行情数据
|
||||
setQuoteLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
logger.debug('useStockQuoteData', '获取股票行情', { stockCode });
|
||||
const quotes = await stockService.getQuotes([stockCode]);
|
||||
if (isCancelled) return;
|
||||
const quoteResult = quotes?.[stockCode] || quotes;
|
||||
const transformedData = transformQuoteData(quoteResult, stockCode);
|
||||
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
|
||||
setQuoteData(transformedData);
|
||||
} catch (err: any) {
|
||||
if (isCancelled || err.name === 'CanceledError') return;
|
||||
logger.error('useStockQuoteData', '获取行情失败', err);
|
||||
setError('获取行情数据失败');
|
||||
setQuoteData(null);
|
||||
} finally {
|
||||
if (!isCancelled) setQuoteLoading(false);
|
||||
}
|
||||
|
||||
// 获取基本信息
|
||||
setBasicLoading(true);
|
||||
try {
|
||||
const { data: result } = await axios.get(`/api/stock/${stockCode}/basic-info`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (isCancelled) return;
|
||||
if (result.success) {
|
||||
setBasicInfo(result.data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (isCancelled || err.name === 'CanceledError') return;
|
||||
logger.error('useStockQuoteData', '获取基本信息失败', err);
|
||||
} finally {
|
||||
if (!isCancelled) setBasicLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
controller.abort();
|
||||
};
|
||||
}, [stockCode]);
|
||||
|
||||
return {
|
||||
quoteData,
|
||||
basicInfo,
|
||||
isLoading: quoteLoading || basicLoading,
|
||||
error,
|
||||
refetch,
|
||||
refetch: refetchRef,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { StockQuoteCardData } from './types';
|
||||
|
||||
/**
|
||||
* 贵州茅台 Mock 数据
|
||||
*/
|
||||
export const mockStockQuoteData: StockQuoteCardData = {
|
||||
// 基础信息
|
||||
name: '贵州茅台',
|
||||
code: '600519.SH',
|
||||
indexTags: ['沪深300'],
|
||||
|
||||
// 价格信息
|
||||
currentPrice: 2178.5,
|
||||
changePercent: 3.65,
|
||||
todayOpen: 2156.0,
|
||||
yesterdayClose: 2101.0,
|
||||
todayHigh: 2185.0,
|
||||
todayLow: 2150.0,
|
||||
|
||||
// 关键指标
|
||||
pe: 38.62,
|
||||
pb: 14.82,
|
||||
marketCap: '2.73万亿',
|
||||
week52Low: 1980,
|
||||
week52High: 2350,
|
||||
|
||||
// 主力动态
|
||||
mainNetInflow: 1.28,
|
||||
institutionHolding: 72.35,
|
||||
buyRatio: 85,
|
||||
sellRatio: 15,
|
||||
|
||||
// 更新时间
|
||||
updateTime: '2025-12-03 14:30:25',
|
||||
|
||||
// 自选状态
|
||||
isFavorite: false,
|
||||
};
|
||||
59
src/views/Company/hooks/useStockSearch.ts
Normal file
59
src/views/Company/hooks/useStockSearch.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* useStockSearch - 股票模糊搜索 Hook
|
||||
*
|
||||
* 提取自 SearchBar.js 和 CompareStockInput.tsx 的共享搜索逻辑
|
||||
* 支持按代码或名称搜索,可选排除指定股票
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export interface Stock {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface UseStockSearchOptions {
|
||||
excludeCode?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 股票模糊搜索 Hook
|
||||
*
|
||||
* @param allStocks - 全部股票列表
|
||||
* @param searchTerm - 搜索关键词
|
||||
* @param options - 可选配置
|
||||
* @param options.excludeCode - 排除的股票代码(用于对比场景)
|
||||
* @param options.limit - 返回结果数量限制,默认 10
|
||||
* @returns 过滤后的股票列表
|
||||
*/
|
||||
export const useStockSearch = (
|
||||
allStocks: Stock[],
|
||||
searchTerm: string,
|
||||
options: UseStockSearchOptions = {}
|
||||
): Stock[] => {
|
||||
const { excludeCode, limit = 10 } = options;
|
||||
|
||||
return useMemo(() => {
|
||||
const trimmed = searchTerm?.trim();
|
||||
if (!trimmed) return [];
|
||||
|
||||
const term = trimmed.toLowerCase();
|
||||
|
||||
return allStocks
|
||||
.filter((stock) => {
|
||||
// 排除指定股票
|
||||
if (excludeCode && stock.code === excludeCode) {
|
||||
return false;
|
||||
}
|
||||
// 按代码或名称匹配
|
||||
return (
|
||||
stock.code.toLowerCase().includes(term) ||
|
||||
stock.name.includes(trimmed)
|
||||
);
|
||||
})
|
||||
.slice(0, limit);
|
||||
}, [allStocks, searchTerm, excludeCode, limit]);
|
||||
};
|
||||
|
||||
export default useStockSearch;
|
||||
Reference in New Issue
Block a user