Merge branch 'feature_bugfix/251113_ui' of https://git.valuefrontier.cn/vf/vf_react into feature_bugfix/251113_ui

This commit is contained in:
2025-11-15 09:11:57 +08:00
28 changed files with 2001 additions and 41 deletions

92
.eslintrc.js Normal file
View File

@@ -0,0 +1,92 @@
module.exports = {
root: true,
/* 环境配置 */
env: {
browser: true,
es2021: true,
node: true,
},
/* 扩展配置 */
extends: [
'react-app', // Create React App 默认规则
'react-app/jest', // Jest 测试规则
'eslint:recommended', // ESLint 推荐规则
'plugin:react/recommended', // React 推荐规则
'plugin:react-hooks/recommended', // React Hooks 规则
'plugin:prettier/recommended', // Prettier 集成
],
/* 解析器选项 */
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
/* 插件 */
plugins: ['react', 'react-hooks', 'prettier'],
/* 规则配置 */
rules: {
// React
'react/react-in-jsx-scope': 'off', // React 17+ 不需要导入 React
'react/prop-types': 'off', // 使用 TypeScript 类型检查,不需要 PropTypes
'react/display-name': 'off', // 允许匿名组件
// 通用
'no-console': ['warn', { allow: ['warn', 'error'] }], // 仅警告 console.log
'no-unused-vars': ['warn', {
argsIgnorePattern: '^_', // 忽略以 _ 开头的未使用参数
varsIgnorePattern: '^_', // 忽略以 _ 开头的未使用变量
}],
'prettier/prettier': ['warn', {}, { usePrettierrc: true }], // 使用项目的 Prettier 配置
},
/* 设置 */
settings: {
react: {
version: 'detect', // 自动检测 React 版本
},
},
/* TypeScript 文件特殊配置 */
overrides: [
{
files: ['**/*.ts', '**/*.tsx'], // 仅对 TS 文件应用以下配置
parser: '@typescript-eslint/parser', // 使用 TypeScript 解析器
parserOptions: {
project: './tsconfig.json', // 关联 tsconfig.json
},
extends: [
'plugin:@typescript-eslint/recommended', // TypeScript 推荐规则
],
plugins: ['@typescript-eslint'],
rules: {
// TypeScript 特定规则
'@typescript-eslint/no-explicit-any': 'warn', // 警告使用 any允许但提示
'@typescript-eslint/explicit-module-boundary-types': 'off', // 不强制导出函数类型
'@typescript-eslint/no-unused-vars': ['warn', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
'@typescript-eslint/no-non-null-assertion': 'warn', // 警告使用 !(非空断言)
// 覆盖基础规则(避免与 TS 规则冲突)
'no-unused-vars': 'off', // 使用 TS 版本的规则
},
},
],
/* 忽略文件(与 .eslintignore 等效)*/
ignorePatterns: [
'node_modules/',
'build/',
'dist/',
'*.config.js',
'public/mockServiceWorker.js',
],
};

159
CLAUDE.md
View File

@@ -20,6 +20,7 @@
**开发指南**: **开发指南**:
- [开发工作流](#开发工作流) - 路由、组件、API、Redux 开发指南 - [开发工作流](#开发工作流) - 路由、组件、API、Redux 开发指南
- [TypeScript 接入](#typescript-接入) - TypeScript 渐进式迁移方案与指南
- [常见开发任务](#常见开发任务) - 5 个详细的开发任务教程 - [常见开发任务](#常见开发任务) - 5 个详细的开发任务教程
- [技术路径与开发指南](#技术路径与开发指南) - UI 框架选型、技术栈演进、最佳实践 - [技术路径与开发指南](#技术路径与开发指南) - UI 框架选型、技术栈演进、最佳实践
@@ -42,6 +43,7 @@
**前端** **前端**
- **核心框架**: React 18.3.1 - **核心框架**: React 18.3.1
- **类型系统**: TypeScript 5.9.3(渐进式接入中,支持 JS/TS 混合开发)
- **UI 组件库**: Chakra UI 2.8.2(主要) + Ant Design 5.27.4(表格/表单) - **UI 组件库**: Chakra UI 2.8.2(主要) + Ant Design 5.27.4(表格/表单)
- **状态管理**: Redux Toolkit 2.9.2 - **状态管理**: Redux Toolkit 2.9.2
- **路由**: React Router v6.30.1 配合 React.lazy() 实现代码分割 - **路由**: React Router v6.30.1 配合 React.lazy() 实现代码分割
@@ -81,6 +83,10 @@ npm test # 运行 React 测试套件CRACO
npm run lint:check # 检查 ESLint 规则(退出码 0 npm run lint:check # 检查 ESLint 规则(退出码 0
npm run lint:fix # 自动修复 ESLint 问题 npm run lint:fix # 自动修复 ESLint 问题
npm run type-check # TypeScript 类型检查(不生成输出)
npm run type-check:watch # TypeScript 类型检查监听模式
npm run clean # 删除 node_modules 和 package-lock.json npm run clean # 删除 node_modules 和 package-lock.json
npm run reinstall # 清洁安装(运行 clean + install npm run reinstall # 清洁安装(运行 clean + install
``` ```
@@ -1386,6 +1392,159 @@ src/views/Community/components/
--- ---
## TypeScript 接入
### 概述
项目已于 **2025-11-13** 完成 TypeScript 环境配置,采用**渐进式迁移策略**,支持 JavaScript 和 TypeScript 混合开发。
**当前状态**: 环境配置完成,准备开始代码迁移
**迁移策略**: 新代码使用 TypeScript旧代码按优先级逐步迁移
**类型严格度**: 推荐模式(`noImplicitAny: true`,其他严格检查待后续开启)
### 已完成的环境配置
**TypeScript 编译器**: v5.9.3
**tsconfig.json**: 推荐模式配置,支持 JS/TS 混合开发
**CRACO 配置**: 支持 `.ts``.tsx` 文件编译
**ESLint 配置**: 支持 TypeScript 语法检查
**路径别名**: 与现有 `@/` 别名保持一致
**全局类型定义**: 基础类型文件已创建在 `src/types/`
### 可用类型定义
项目已创建以下基础类型定义文件:
**`src/types/api.ts`** - API 相关类型
- `ApiResponse<T>` - 通用 API 响应结构
- `PaginatedResponse<T>` - 分页响应
- `ApiError` - API 错误类型
- `ListQueryParams` - 列表查询参数
**`src/types/stock.ts`** - 股票相关类型
- `StockInfo` - 股票基础信息
- `StockQuote` - 股票行情数据
- `KLineData` - K 线数据
- `StockFinancials` - 财务指标
- `StockPosition` - 股票持仓
- `Sector` - 概念/行业板块
**`src/types/user.ts`** - 用户相关类型
- `UserInfo` - 用户基础信息
- `AuthInfo` - 认证信息
- `LoginParams` / `RegisterParams` - 登录/注册参数
- `UserSubscription` - 订阅信息
- `UserAccount` - 资金账户
- `UserSettings` - 用户设置
**使用方式**:
```typescript
// 统一导入
import type { StockQuote, UserInfo, ApiResponse } from '@/types';
// 或从具体文件导入
import type { StockQuote } from '@/types/stock';
```
### TypeScript 命令
```bash
# 类型检查(不生成输出文件)
npm run type-check
# 类型检查 + 监听模式
npm run type-check:watch
# ESLint 检查(包含 TS 文件)
npm run lint:check
# ESLint 自动修复
npm run lint:fix
```
### 迁移路线图
详细的迁移指南请参考 **[TYPESCRIPT_MIGRATION.md](./TYPESCRIPT_MIGRATION.md)** 文档。
**简要路线图**:
1. **优先级 1⃣**: 工具层(`src/utils/`, `src/constants/`
- 纯函数,迁移成本低,收益高
- 提供类型定义给其他模块使用
2. **优先级 2⃣**: 类型定义层(扩展 `src/types/`
- 添加 `trading.ts`, `community.ts`, `chart.ts`
3. **优先级 3⃣**: 服务层(`src/services/`
- 定义 API 请求/响应类型
- 使用 `ApiResponse<T>` 包装响应
4. **优先级 4⃣**: Redux 状态层(`src/store/slices/`
- 定义 `RootState``AppDispatch` 类型
- 创建类型化的 hooks
5. **优先级 5⃣**: 自定义 Hooks`src/hooks/`
- 添加泛型支持
- 定义完整返回值类型
6. **优先级 6⃣**: 组件层(`src/components/`, `src/views/`
- Atoms → Molecules → Organisms → Pages
- 优先迁移复用度高的组件
### 开发规范
**新代码**:
-**必须使用 TypeScript**`.ts``.tsx`
- ✅ 所有函数参数和返回值添加类型
- ✅ 组件 Props 使用 `interface` 定义
- ✅ 避免使用 `any`(特殊情况需添加注释说明)
**旧代码迁移**:
- 按优先级迁移,不强制一次性完成
- 迁移前先阅读 [TYPESCRIPT_MIGRATION.md](./TYPESCRIPT_MIGRATION.md)
- 迁移后运行 `npm run type-check` 验证
**类型定义**:
- 公共类型定义导出到 `src/types/`
- 组件内部类型可定义在组件文件中
- 使用 `type` 还是 `interface` 参考 [迁移指南](./TYPESCRIPT_MIGRATION.md)
### 常见问题
**Q: 路径别名 `@/types` 无法识别?**
A: 确保在 `tsconfig.json` 中配置了 `paths`,并重启 IDE。使用 `npm run type-check` 而非命令行 `tsc`
**Q: 如何处理第三方库没有类型定义?**
A:
1. 尝试安装 `@types/library-name`
2. 创建自定义类型声明文件 `src/types/library-name.d.ts`
3. 临时使用 `as any`(需添加 TODO 注释)
**Q: 迁移期间如何处理 any 类型?**
A: 添加 ESLint 禁用注释和 TODO 说明:
```typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
// TODO: 待完善类型定义
const legacyFunction = (data: any): any => { ... };
```
**Q: React 组件的 children 类型如何定义?**
A: 使用 `React.ReactNode`
```typescript
interface Props {
children: React.ReactNode;
}
```
### 参考资源
- [TYPESCRIPT_MIGRATION.md](./TYPESCRIPT_MIGRATION.md) - 完整迁移指南
- [TypeScript 官方文档](https://www.typescriptlang.org/docs/)
- [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/)
- [Redux Toolkit TypeScript 指南](https://redux-toolkit.js.org/usage/usage-with-typescript)
---
## 更新本文档 ## 更新本文档
本 CLAUDE.md 文件是一个持续更新的文档。在以下情况下应更新它: 本 CLAUDE.md 文件是一个持续更新的文档。在以下情况下应更新它:

View File

@@ -133,8 +133,8 @@ module.exports = {
'@variables': path.resolve(__dirname, 'src/variables'), '@variables': path.resolve(__dirname, 'src/variables'),
'@views': path.resolve(__dirname, 'src/views'), '@views': path.resolve(__dirname, 'src/views'),
}, },
// 减少文件扩展名搜索 // 减少文件扩展名搜索(优先 TypeScript
extensions: ['.js', '.jsx', '.json'], extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
// 优化模块查找路径 // 优化模块查找路径
modules: [ modules: [
path.resolve(__dirname, 'src'), path.resolve(__dirname, 'src'),

View File

@@ -1,27 +0,0 @@
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["./*"],
"@assets/*": ["assets/*"],
"@components/*": ["components/*"],
"@constants/*": ["constants/*"],
"@contexts/*": ["contexts/*"],
"@data/*": ["data/*"],
"@hooks/*": ["hooks/*"],
"@layouts/*": ["layouts/*"],
"@lib/*": ["lib/*"],
"@mocks/*": ["mocks/*"],
"@providers/*": ["providers/*"],
"@routes/*": ["routes/*"],
"@services/*": ["services/*"],
"@store/*": ["store/*"],
"@styles/*": ["styles/*"],
"@theme/*": ["theme/*"],
"@utils/*": ["utils/*"],
"@variables/*": ["variables/*"],
"@views/*": ["views/*"]
}
},
"exclude": ["node_modules", "build", "dist"]
}

View File

@@ -106,13 +106,20 @@
"deploy": "bash scripts/deploy-from-local.sh", "deploy": "bash scripts/deploy-from-local.sh",
"deploy:setup": "bash scripts/setup-deployment.sh", "deploy:setup": "bash scripts/setup-deployment.sh",
"rollback": "bash scripts/rollback-from-local.sh", "rollback": "bash scripts/rollback-from-local.sh",
"lint:check": "eslint . --ext=js,jsx; exit 0", "lint:check": "eslint . --ext=js,jsx,ts,tsx; exit 0",
"lint:fix": "eslint . --ext=js,jsx --fix; exit 0", "lint:fix": "eslint . --ext=js,jsx,ts,tsx --fix; exit 0",
"type-check": "tsc --noEmit",
"type-check:watch": "tsc --noEmit --watch",
"clean": "rm -rf node_modules/ package-lock.json", "clean": "rm -rf node_modules/ package-lock.json",
"reinstall": "npm run clean && npm install" "reinstall": "npm run clean && npm install"
}, },
"devDependencies": { "devDependencies": {
"@craco/craco": "^7.1.0", "@craco/craco": "^7.1.0",
"@types/node": "^20.19.25",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@typescript-eslint/eslint-plugin": "^8.46.4",
"@typescript-eslint/parser": "^8.46.4",
"ajv": "^8.17.1", "ajv": "^8.17.1",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
@@ -131,6 +138,7 @@
"react-error-overlay": "6.0.9", "react-error-overlay": "6.0.9",
"sharp": "^0.34.4", "sharp": "^0.34.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.9.3",
"webpack-bundle-analyzer": "^4.10.2", "webpack-bundle-analyzer": "^4.10.2",
"yn": "^5.1.0" "yn": "^5.1.0"
}, },

View File

@@ -0,0 +1,77 @@
// src/components/EventCommentSection/CommentInput.js
/**
* 评论输入框组件
* 功能:输入评论内容、字数限制、发布按钮
*/
import React from 'react';
import {
Box,
Textarea,
Button,
HStack,
Text,
useColorModeValue,
} from '@chakra-ui/react';
const CommentInput = ({
value,
onChange,
onSubmit,
isSubmitting,
maxLength = 500,
placeholder = '说点什么...',
}) => {
const bgColor = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const textColor = useColorModeValue('gray.600', 'gray.400');
const countColor = useColorModeValue('gray.500', 'gray.500');
const handleKeyDown = (e) => {
// Ctrl/Cmd + Enter 快捷键提交
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
onSubmit();
}
};
return (
<Box>
<Textarea
value={value}
onChange={onChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
bg={bgColor}
borderColor={borderColor}
color={textColor}
rows={3}
maxLength={maxLength}
resize="vertical"
_hover={{
borderColor: 'blue.300',
}}
_focus={{
borderColor: 'blue.500',
boxShadow: '0 0 0 1px var(--chakra-colors-blue-500)',
}}
/>
<HStack justify="space-between" mt={2}>
<Text fontSize="sm" color={countColor}>
{value.length}/{maxLength}
{value.length === 0 && ' · Ctrl+Enter 快速发布'}
</Text>
<Button
colorScheme="blue"
size="sm"
onClick={onSubmit}
isLoading={isSubmitting}
isDisabled={!value.trim() || isSubmitting}
>
发布
</Button>
</HStack>
</Box>
);
};
export default CommentInput;

View File

@@ -0,0 +1,89 @@
// src/components/EventCommentSection/CommentItem.js
/**
* 单条评论组件
* 功能:显示用户头像、昵称、时间、评论内容
*/
import React from 'react';
import {
Box,
HStack,
VStack,
Avatar,
Text,
useColorModeValue,
} from '@chakra-ui/react';
import moment from 'moment';
import 'moment/locale/zh-cn';
moment.locale('zh-cn');
const CommentItem = ({ comment }) => {
const itemBg = useColorModeValue('gray.50', 'gray.700');
const usernameColor = useColorModeValue('gray.800', 'gray.100');
const timeColor = useColorModeValue('gray.500', 'gray.400');
const contentColor = useColorModeValue('gray.700', 'gray.300');
// 格式化时间
const formatTime = (timestamp) => {
const now = moment();
const time = moment(timestamp);
const diffMinutes = now.diff(time, 'minutes');
const diffHours = now.diff(time, 'hours');
const diffDays = now.diff(time, 'days');
if (diffMinutes < 1) {
return '刚刚';
} else if (diffMinutes < 60) {
return `${diffMinutes}分钟前`;
} else if (diffHours < 24) {
return `${diffHours}小时前`;
} else if (diffDays < 7) {
return `${diffDays}天前`;
} else {
return time.format('MM-DD HH:mm');
}
};
return (
<Box
p={3}
bg={itemBg}
borderRadius="md"
transition="all 0.2s"
_hover={{
transform: 'translateY(-2px)',
boxShadow: 'sm',
}}
>
<HStack align="start" spacing={3}>
{/* 用户头像 */}
<Avatar
size="sm"
name={comment.author?.username || 'Anonymous'}
src={comment.author?.avatar}
/>
{/* 评论内容区 */}
<VStack align="stretch" flex={1} spacing={1}>
{/* 用户名和时间 */}
<HStack spacing={2}>
<Text fontSize="sm" fontWeight="bold" color={usernameColor}>
{comment.author?.username || 'Anonymous'}
</Text>
<Text fontSize="xs" color={timeColor}>
{formatTime(comment.created_at)}
</Text>
</HStack>
{/* 评论内容 */}
<Text fontSize="sm" color={contentColor} lineHeight="1.6">
{comment.content}
</Text>
</VStack>
</HStack>
</Box>
);
};
export default CommentItem;

View File

@@ -0,0 +1,67 @@
// src/components/EventCommentSection/CommentList.js
/**
* 评论列表组件
* 功能:展示评论列表、加载状态、空状态
*/
import React from 'react';
import {
VStack,
Spinner,
Center,
Text,
Box,
useColorModeValue,
} from '@chakra-ui/react';
import { ChatIcon } from '@chakra-ui/icons';
import CommentItem from './CommentItem';
const CommentList = ({ comments, loading }) => {
const emptyTextColor = useColorModeValue('gray.500', 'gray.400');
const emptyBgColor = useColorModeValue('gray.50', 'gray.700');
// 加载状态
if (loading) {
return (
<Center py={8}>
<VStack spacing={3}>
<Spinner size="md" color="blue.500" thickness="3px" />
<Text fontSize="sm" color={emptyTextColor}>
加载评论中...
</Text>
</VStack>
</Center>
);
}
// 空状态
if (!comments || comments.length === 0) {
return (
<Center py={8}>
<VStack spacing={3}>
<Box
p={4}
bg={emptyBgColor}
borderRadius="full"
>
<ChatIcon boxSize={6} color={emptyTextColor} />
</Box>
<Text fontSize="sm" color={emptyTextColor}>
还没有评论快来发表第一条吧~
</Text>
</VStack>
</Center>
);
}
// 评论列表
return (
<VStack align="stretch" spacing={3}>
{comments.map((comment) => (
<CommentItem key={comment.id} comment={comment} />
))}
</VStack>
);
};
export default CommentList;

View File

@@ -0,0 +1,245 @@
// src/components/EventCommentSection/EventCommentSection.tsx
/**
* 事件评论区主组件TypeScript 版本)
* 功能:整合评论列表 + 评论输入框,管理评论数据
* 使用 usePagination Hook 实现分页功能
*/
import React, { useState, useCallback } from 'react';
import {
Box,
Heading,
Badge,
HStack,
Divider,
useColorModeValue,
useToast,
Button,
Center,
} from '@chakra-ui/react';
import { useAuth } from '../../contexts/AuthContext';
import { eventService } from '../../services/eventService';
import { logger } from '../../utils/logger';
import { usePagination } from '../../hooks/usePagination';
import type { Comment, CreateCommentParams } from '@/types';
import type { PaginationLoadResult } from '@/types';
import CommentList from './CommentList';
import CommentInput from './CommentInput';
/**
* 组件 Props
*/
interface EventCommentSectionProps {
/** 事件 ID */
eventId: string | number;
}
/**
* 事件评论区组件
*/
const EventCommentSection: React.FC<EventCommentSectionProps> = ({ eventId }) => {
const { user } = useAuth();
const toast = useToast();
const dividerColor = useColorModeValue('gray.200', 'gray.600');
const headingColor = useColorModeValue('gray.700', 'gray.200');
const sectionBg = useColorModeValue('gray.50', 'gray.750');
// 评论输入状态
const [commentText, setCommentText] = useState('');
const [submitting, setSubmitting] = useState(false);
/**
* 加载评论数据的函数
* @param page 页码
* @param append 是否追加到已有数据
* @returns 分页响应数据
*/
const loadCommentsFunction = useCallback(
async (page: number, append: boolean): Promise<PaginationLoadResult<Comment>> => {
try {
const result = await eventService.getPosts(
eventId,
'latest',
page,
5 // 每页 5 条评论
);
if (result.success) {
logger.info('EventCommentSection', '评论加载成功', {
eventId,
page,
count: result.data?.length || 0,
total: result.pagination?.total || 0,
append,
});
return {
data: result.data || [],
pagination: result.pagination,
};
} else {
throw new Error(result.message || '加载评论失败');
}
} catch (error: any) {
logger.error('EventCommentSection', 'loadCommentsFunction', error, {
eventId,
page,
});
toast({
title: '加载评论失败',
description: error.message || '请稍后重试',
status: 'error',
duration: 3000,
isClosable: true,
});
throw error;
}
},
[eventId, toast]
);
// 使用 usePagination Hook
const {
data: comments,
loading,
loadingMore,
hasMore,
totalCount,
loadMore,
setData: setComments,
setTotalCount,
} = usePagination<Comment>(loadCommentsFunction, {
pageSize: 5,
autoLoad: true,
});
/**
* 发表评论
*/
const handleSubmitComment = useCallback(async () => {
if (!commentText.trim()) {
toast({
title: '请输入评论内容',
status: 'warning',
duration: 2000,
isClosable: true,
});
return;
}
setSubmitting(true);
try {
const params: CreateCommentParams = {
content: commentText.trim(),
content_type: 'text',
};
const result = await eventService.createPost(eventId, params);
if (result.success) {
// 乐观更新:立即将新评论添加到本地 state避免重新加载导致的闪烁
const newComment: Comment = {
id: result.data?.id || `comment_optimistic_${Date.now()}`,
content: commentText.trim(),
content_type: 'text',
author: {
id: user?.id || 'current_user',
username: user?.username || '当前用户',
avatar: user?.avatar || null,
},
created_at: new Date().toISOString(),
likes_count: 0,
is_liked: false,
};
// 将新评论追加到列表末尾(最新评论在底部)
setComments((prevComments) => [...prevComments, newComment]);
// 总评论数 +1
setTotalCount((prevTotal) => prevTotal + 1);
toast({
title: '评论发布成功',
status: 'success',
duration: 2000,
isClosable: true,
});
setCommentText(''); // 清空输入框
logger.info('EventCommentSection', '评论发布成功(乐观更新)', {
eventId,
content: commentText.trim(),
commentId: newComment.id,
});
} else {
throw new Error(result.message || '评论发布失败');
}
} catch (error: any) {
logger.error('EventCommentSection', 'handleSubmitComment', error, { eventId });
toast({
title: '评论发布失败',
description: error.message || '请稍后重试',
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setSubmitting(false);
}
}, [eventId, commentText, toast, user, setComments, setTotalCount]);
return (
<Box>
{/* 标题栏 */}
<HStack spacing={3} mb={4} p={3} bg={sectionBg} borderRadius="md">
<Heading size="sm" color={headingColor}>
</Heading>
<Badge colorScheme="blue" fontSize="sm" borderRadius="full" px={2}>
{totalCount}
</Badge>
</HStack>
<Divider borderColor={dividerColor} mb={4} />
{/* 评论列表 */}
<Box mb={4}>
<CommentList comments={comments} loading={loading} />
</Box>
{/* 加载更多按钮(仅当有更多评论时显示) */}
{hasMore && (
<Center mb={4}>
<Button
variant="outline"
colorScheme="blue"
size="sm"
onClick={loadMore}
isLoading={loadingMore}
loadingText="加载中..."
>
</Button>
</Center>
)}
{/* 评论输入框(仅登录用户显示) */}
{user && (
<Box>
<Divider borderColor={dividerColor} mb={4} />
<CommentInput
value={commentText}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
setCommentText(e.target.value)
}
onSubmit={handleSubmitComment}
isSubmitting={submitting}
maxLength={500}
placeholder="说点什么..."
/>
</Box>
)}
</Box>
);
};
export default EventCommentSection;

View File

@@ -0,0 +1,10 @@
// src/components/EventCommentSection/index.js
/**
* 事件评论区组件统一导出
*/
export { default } from './EventCommentSection';
export { default as EventCommentSection } from './EventCommentSection';
export { default as CommentList } from './CommentList';
export { default as CommentItem } from './CommentItem';
export { default as CommentInput } from './CommentInput';

View File

@@ -64,6 +64,9 @@ export const NotificationProvider = ({ children }) => {
const adaptEventToNotificationRef = useRef(null); const adaptEventToNotificationRef = useRef(null);
const isFirstConnect = useRef(true); // 标记是否首次连接 const isFirstConnect = useRef(true); // 标记是否首次连接
// ⚡ 事件更新回调列表(用于在收到 new_event 时通知其他组件刷新数据)
const eventUpdateCallbacks = useRef(new Set());
// ⚡ 使用权限引导管理 Hook // ⚡ 使用权限引导管理 Hook
const { shouldShowGuide, markGuideAsShown } = usePermissionGuide(); const { shouldShowGuide, markGuideAsShown } = usePermissionGuide();
@@ -160,6 +163,37 @@ export const NotificationProvider = ({ children }) => {
}); });
}, []); }, []);
/**
* 注册事件更新回调(用于在收到新事件时通知其他组件刷新)
* @param {Function} callback - 回调函数,接收 eventData 参数
* @returns {Function} 取消注册函数
*/
const registerEventUpdateCallback = useCallback((callback) => {
eventUpdateCallbacks.current.add(callback);
logger.info('NotificationContext', 'Event update callback registered', {
totalCallbacks: eventUpdateCallbacks.current.size
});
// 返回取消注册函数
return () => {
eventUpdateCallbacks.current.delete(callback);
logger.info('NotificationContext', 'Event update callback unregistered', {
totalCallbacks: eventUpdateCallbacks.current.size
});
};
}, []);
/**
* 取消注册事件更新回调(已废弃,建议使用 registerEventUpdateCallback 返回的函数)
* @param {Function} callback - 要取消的回调函数
*/
const unregisterEventUpdateCallback = useCallback((callback) => {
eventUpdateCallbacks.current.delete(callback);
logger.info('NotificationContext', 'Event update callback unregistered (manual)', {
totalCallbacks: eventUpdateCallbacks.current.size
});
}, []);
/** /**
* 请求浏览器通知权限 * 请求浏览器通知权限
*/ */
@@ -764,6 +798,21 @@ export const NotificationProvider = ({ children }) => {
console.log('[NotificationContext] 准备添加通知到队列...'); console.log('[NotificationContext] 准备添加通知到队列...');
addNotificationRef.current(notification); addNotificationRef.current(notification);
console.log('[NotificationContext] ✅ 通知已添加到队列'); console.log('[NotificationContext] ✅ 通知已添加到队列');
// ⚡ 调用所有注册的事件更新回调(用于通知其他组件刷新数据)
if (eventUpdateCallbacks.current.size > 0) {
console.log(`[NotificationContext] 🔔 触发 ${eventUpdateCallbacks.current.size} 个事件更新回调...`);
eventUpdateCallbacks.current.forEach(callback => {
try {
callback(data);
} catch (error) {
logger.error('NotificationContext', 'Event update callback error', error);
console.error('[NotificationContext] ❌ 事件更新回调执行失败:', error);
}
});
console.log('[NotificationContext] ✅ 所有事件更新回调已触发');
}
console.log('%c════════════════════════════════════════\n', 'color: #FF9800; font-weight: bold;'); console.log('%c════════════════════════════════════════\n', 'color: #FF9800; font-weight: bold;');
}); });
@@ -1040,6 +1089,9 @@ export const NotificationProvider = ({ children }) => {
showWelcomeGuide, showWelcomeGuide,
showCommunityGuide, showCommunityGuide,
showFirstFollowGuide, showFirstFollowGuide,
// ⚡ 新增:事件更新回调注册方法
registerEventUpdateCallback,
unregisterEventUpdateCallback,
}; };
return ( return (

137
src/hooks/usePagination.ts Normal file
View File

@@ -0,0 +1,137 @@
/**
* usePagination - 通用分页 Hook
*
* 封装分页逻辑,支持初始加载、加载更多、重置等功能
*
* @example
* const {
* data: comments,
* loading,
* loadingMore,
* hasMore,
* totalCount,
* loadMore,
* setData,
* setTotalCount,
* } = usePagination<Comment>(loadCommentsFunction, { pageSize: 5 });
*/
import { useState, useCallback, useEffect } from 'react';
import type {
LoadFunction,
PaginationLoadResult,
UsePaginationOptions,
UsePaginationResult,
} from '@/types/pagination';
/**
* usePagination Hook
* @template T 数据项类型
* @param loadFunction 加载函数,接收 (page, append) 参数
* @param options 配置选项
* @returns 分页状态和操作方法
*/
export function usePagination<T>(
loadFunction: LoadFunction<T>,
options: UsePaginationOptions = {}
): UsePaginationResult<T> {
const { pageSize = 10, autoLoad = true } = options;
// 状态管理
const [data, setData] = useState<T[]>([]);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const [hasMore, setHasMore] = useState(false);
/**
* 加载数据
* @param page 页码
* @param append 是否追加true: 追加false: 替换)
*/
const loadData = useCallback(
async (page: number, append: boolean = false) => {
// 设置加载状态
if (append) {
setLoadingMore(true);
} else {
setLoading(true);
}
try {
const result: PaginationLoadResult<T> = await loadFunction(page, append);
// 更新数据
if (append) {
setData((prevData) => [...prevData, ...(result.data || [])]);
} else {
setData(result.data || []);
}
// 更新分页信息
const total = result.pagination?.total || result.data?.length || 0;
setTotalCount(total);
// 计算是否还有更多数据
const currentTotal = append
? data.length + (result.data?.length || 0)
: result.data?.length || 0;
setHasMore(currentTotal < total);
} catch (error) {
console.error('[usePagination] 加载数据失败:', error);
throw error;
} finally {
if (append) {
setLoadingMore(false);
} else {
setLoading(false);
}
}
},
[loadFunction, data.length]
);
/**
* 加载更多数据
*/
const loadMore = useCallback(async () => {
if (loadingMore || !hasMore) return;
const nextPage = currentPage + 1;
await loadData(nextPage, true);
setCurrentPage(nextPage);
}, [currentPage, loadData, loadingMore, hasMore]);
/**
* 重置到第一页
*/
const reset = useCallback(() => {
setCurrentPage(1);
setData([]);
setTotalCount(0);
setHasMore(false);
loadData(1, false);
}, [loadData]);
// 自动加载第一页
useEffect(() => {
if (autoLoad) {
loadData(1, false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoLoad]);
return {
data,
loading,
loadingMore,
currentPage,
hasMore,
totalCount,
loadMore,
reset,
setData,
setTotalCount,
};
}

View File

@@ -9,6 +9,91 @@ import { generatePopularConcepts } from './concept';
// 模拟网络延迟 // 模拟网络延迟
const delay = (ms = 300) => new Promise(resolve => setTimeout(resolve, ms)); const delay = (ms = 300) => new Promise(resolve => setTimeout(resolve, ms));
// ==================== 评论内存存储 ====================
// 用于在 Mock 环境下持久化评论数据(按 eventId 分组)
const commentsStore = new Map();
/**
* 初始化某个事件的 mock 评论列表
* @param {string} eventId - 事件 ID
* @returns {Array} 初始的 15 条 mock 评论
*/
const initializeMockComments = (eventId) => {
const comments = [];
const users = [
{ username: '张三', avatar: null },
{ username: '李四', avatar: null },
{ username: '王五', avatar: null },
{ username: '赵六', avatar: null },
{ username: '投资达人', avatar: null },
{ username: '价值投资者', avatar: null },
{ username: '技术分析师', avatar: null },
{ username: '基本面研究员', avatar: null },
{ username: '量化交易员', avatar: null },
{ username: '市场观察者', avatar: null },
{ username: '行业分析师', avatar: null },
{ username: '财经评论员', avatar: null },
{ username: '趋势跟踪者', avatar: null },
{ username: '价值发现者', avatar: null },
{ username: '理性投资人', avatar: null },
];
const commentTemplates = [
'这个事件对相关板块影响很大,值得关注后续发展',
'相关概念股已经开始异动了,市场反应很快',
'感谢分享,这个事件我之前没注意到',
'从基本面来看,这个事件会带来实质性利好',
'需要观察后续政策落地情况,现在下结论还太早',
'相关产业链的龙头企业值得重点关注',
'这类事件一般都是短期刺激,长期影响有限',
'建议大家理性对待,不要盲目追高',
'这个消息已经在预期之中,股价可能提前反应了',
'关键要看后续的执行力度和落地速度',
'建议关注产业链上下游的投资机会',
'短期可能会有波动,但长期逻辑依然成立',
'市场情绪很高涨,需要警惕追高风险',
'从历史数据来看,类似事件后续表现都不错',
'这是一个结构性机会,需要精选个股',
];
for (let i = 0; i < 15; i++) {
const hoursAgo = Math.floor(Math.random() * 48) + 1; // 1-48 小时前
const createdAt = new Date(Date.now() - hoursAgo * 60 * 60 * 1000);
const user = users[i % users.length];
comments.push({
id: `comment_${eventId}_${i + 1}`,
content: commentTemplates[i % commentTemplates.length],
content_type: 'text',
author: {
id: `user_${i + 1}`,
username: user.username,
avatar: user.avatar,
},
created_at: createdAt.toISOString(),
likes_count: Math.floor(Math.random() * 20),
is_liked: false,
});
}
// 按时间升序排序(最旧的在前)
comments.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
return comments;
};
/**
* 获取或初始化评论列表
* @param {string} eventId - 事件 ID
* @returns {Array} 评论列表
*/
const getOrInitComments = (eventId) => {
if (!commentsStore.has(eventId)) {
commentsStore.set(eventId, initializeMockComments(eventId));
}
return commentsStore.get(eventId);
};
export const eventHandlers = [ export const eventHandlers = [
// ==================== 事件列表相关 ==================== // ==================== 事件列表相关 ====================
@@ -1249,4 +1334,111 @@ export const eventHandlers = [
); );
} }
}), }),
// ==================== 评论相关 ====================
// 获取事件评论列表
http.get('/api/events/:eventId/posts', async ({ params, request }) => {
await delay(300);
const { eventId } = params;
const url = new URL(request.url);
const sort = url.searchParams.get('sort') || 'latest';
const page = parseInt(url.searchParams.get('page') || '1');
const perPage = parseInt(url.searchParams.get('per_page') || '20');
console.log('[Mock] 获取评论列表, eventId:', eventId, 'sort:', sort);
try {
// 从内存存储获取评论列表
const allComments = getOrInitComments(eventId);
// ✅ 创建副本并排序(避免直接修改原数组)
let sortedComments = [...allComments];
if (sort === 'hot') {
sortedComments.sort((a, b) => b.likes_count - a.likes_count);
} else {
// 默认按时间升序oldest first- 最旧评论在前,最新在后
sortedComments.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
}
// 分页处理(使用排序后的副本)
const startIndex = (page - 1) * perPage;
const endIndex = startIndex + perPage;
const paginatedComments = sortedComments.slice(startIndex, endIndex);
return HttpResponse.json({
success: true,
data: paginatedComments,
pagination: {
page: page,
per_page: perPage,
total: allComments.length,
pages: Math.ceil(allComments.length / perPage),
has_prev: page > 1,
has_next: endIndex < allComments.length,
},
message: '获取评论成功',
});
} catch (error) {
console.error('[Mock] 获取评论列表失败:', error);
return HttpResponse.json(
{
success: false,
error: '获取评论失败',
data: [],
},
{ status: 500 }
);
}
}),
// 发表评论
http.post('/api/events/:eventId/posts', async ({ params, request }) => {
await delay(500);
const { eventId } = params;
const body = await request.json();
console.log('[Mock] 发表评论, eventId:', eventId, 'content:', body.content);
try {
// 模拟创建新评论
const newComment = {
id: `comment_${eventId}_${Date.now()}`,
content: body.content,
content_type: body.content_type || 'text',
author: {
id: 'current_user',
username: '当前用户',
avatar: null,
},
created_at: new Date().toISOString(),
likes_count: 0,
is_liked: false,
};
// 将新评论添加到内存存储(插入到列表开头)
const comments = getOrInitComments(eventId);
comments.unshift(newComment);
console.log('[Mock] 评论已添加到内存存储, 当前评论总数:', comments.length);
return HttpResponse.json({
success: true,
data: newComment,
message: '评论发布成功',
});
} catch (error) {
console.error('[Mock] 发表评论失败:', error);
return HttpResponse.json(
{
success: false,
error: '评论发布失败',
message: '系统错误,请稍后重试',
},
{ status: 500 }
);
}
}),
]; ];

68
src/types/api.ts Normal file
View File

@@ -0,0 +1,68 @@
/**
* API 相关类型定义
*
* 包含通用的 API 请求/响应类型、错误类型等
*/
/**
* API 响应基础结构
*/
export interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
}
/**
* API 错误类型
*/
export interface ApiError {
code: number;
message: string;
details?: any;
}
/**
* 分页请求参数
*/
export interface PaginationParams {
page: number;
pageSize: number;
total?: number;
}
/**
* 分页响应数据
*/
export interface PaginatedResponse<T> {
list: T[];
pagination: {
current: number;
pageSize: number;
total: number;
totalPages: number;
};
}
/**
* 排序参数
*/
export interface SortParams {
field: string;
order: 'asc' | 'desc';
}
/**
* 过滤参数
*/
export interface FilterParams {
[key: string]: any;
}
/**
* 通用列表查询参数
*/
export interface ListQueryParams extends Partial<PaginationParams> {
sort?: SortParams;
filters?: FilterParams;
}

42
src/types/comment.ts Normal file
View File

@@ -0,0 +1,42 @@
/**
* 评论相关类型定义
*/
/**
* 评论作者信息
*/
export interface CommentAuthor {
id: string;
username: string;
avatar?: string | null;
}
/**
* 评论数据结构
*/
export interface Comment {
/** 评论 ID */
id: string;
/** 评论内容 */
content: string;
/** 内容类型 */
content_type: 'text' | 'image' | 'video';
/** 作者信息 */
author: CommentAuthor;
/** 创建时间ISO 8601 格式) */
created_at: string;
/** 点赞数 */
likes_count: number;
/** 当前用户是否已点赞 */
is_liked: boolean;
}
/**
* 创建评论请求参数
*/
export interface CreateCommentParams {
/** 评论内容 */
content: string;
/** 内容类型 */
content_type?: 'text' | 'image' | 'video';
}

55
src/types/index.ts Normal file
View File

@@ -0,0 +1,55 @@
/**
* 全局类型定义汇总导出
*
* 使用方式:
* import { StockQuote, UserInfo, ApiResponse } from '@/types';
*/
// API 相关类型
export type {
ApiResponse,
ApiError,
PaginationParams,
PaginatedResponse,
SortParams,
FilterParams,
ListQueryParams,
} from './api';
// 股票相关类型
export type {
StockInfo,
StockQuote,
KLineData,
StockFinancials,
StockPosition,
Sector,
StockSearchResult,
} from './stock';
// 用户相关类型
export type {
UserInfo,
AuthInfo,
LoginParams,
RegisterParams,
UserPermissions,
UserSubscription,
UserAccount,
UserSettings,
} from './user';
// 分页相关类型
export type {
LoadFunction,
PaginationLoadResult,
UsePaginationOptions,
UsePaginationResult,
} from './pagination';
// 评论相关类型
export type {
Comment,
CommentAuthor,
CreateCommentParams,
} from './comment';

72
src/types/pagination.ts Normal file
View File

@@ -0,0 +1,72 @@
/**
* 分页相关类型定义
*
* 用于 usePagination Hook 和其他分页功能
*/
/**
* 分页加载函数类型
* @template T 数据项类型
* @param page 页码(从 1 开始)
* @param append 是否追加到已有数据true: 追加false: 替换)
* @returns Promise解析为分页响应数据
*/
export type LoadFunction<T> = (
page: number,
append: boolean
) => Promise<PaginationLoadResult<T>>;
/**
* 分页加载结果
* @template T 数据项类型
*/
export interface PaginationLoadResult<T> {
/** 数据列表 */
data: T[];
/** 分页信息 */
pagination?: {
/** 总数据量 */
total?: number;
/** 当前页码 */
page?: number;
/** 每页数量 */
per_page?: number;
};
}
/**
* usePagination Hook 配置选项
*/
export interface UsePaginationOptions {
/** 每页数据量,默认 10 */
pageSize?: number;
/** 是否自动加载第一页,默认 true */
autoLoad?: boolean;
}
/**
* usePagination Hook 返回值
* @template T 数据项类型
*/
export interface UsePaginationResult<T> {
/** 当前数据列表 */
data: T[];
/** 是否正在加载第一页 */
loading: boolean;
/** 是否正在加载更多 */
loadingMore: boolean;
/** 当前页码 */
currentPage: number;
/** 是否还有更多数据 */
hasMore: boolean;
/** 总数据量 */
totalCount: number;
/** 加载更多数据 */
loadMore: () => Promise<void>;
/** 重置到第一页 */
reset: () => void;
/** 手动设置数据(用于乐观更新) */
setData: React.Dispatch<React.SetStateAction<T[]>>;
/** 手动设置总数(用于乐观更新) */
setTotalCount: React.Dispatch<React.SetStateAction<number>>;
}

103
src/types/stock.ts Normal file
View File

@@ -0,0 +1,103 @@
/**
* 股票相关类型定义
*
* 包含股票数据、行情、指标等类型
*/
/**
* 股票基础信息
*/
export interface StockInfo {
code: string; // 股票代码(如 '600000.SH'
name: string; // 股票名称
market: 'SH' | 'SZ' | 'BJ'; // 市场(上海/深圳/北京)
industry?: string; // 所属行业
concept?: string[]; // 所属概念
listingDate?: string; // 上市日期
}
/**
* 股票行情数据
*/
export interface StockQuote {
code: string; // 股票代码
name: string; // 股票名称
current: number; // 当前价格
open: number; // 开盘价
close: number; // 收盘价(前一交易日)
high: number; // 最高价
low: number; // 最低价
volume: number; // 成交量
amount: number; // 成交额
change: number; // 涨跌额
changePercent: number; // 涨跌幅(百分比)
turnoverRate?: number; // 换手率
timestamp: number; // 时间戳
}
/**
* K 线数据(日线/分钟线)
*/
export interface KLineData {
date: string; // 日期YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss
open: number; // 开盘价
high: number; // 最高价
low: number; // 最低价
close: number; // 收盘价
volume: number; // 成交量
amount?: number; // 成交额
}
/**
* 股票财务指标
*/
export interface StockFinancials {
code: string; // 股票代码
reportDate: string; // 报告期
revenue: number; // 营业收入
netProfit: number; // 净利润
eps: number; // 每股收益EPS
roe: number; // 净资产收益率ROE
pe: number; // 市盈率PE
pb: number; // 市净率PB
grossProfitMargin?: number; // 毛利率
debtToAssetRatio?: number; // 资产负债率
}
/**
* 股票持仓
*/
export interface StockPosition {
code: string; // 股票代码
name: string; // 股票名称
quantity: number; // 持仓数量
availableQuantity: number; // 可用数量T+1
costPrice: number; // 成本价
currentPrice: number; // 当前价
marketValue: number; // 市值
profit: number; // 盈亏金额
profitPercent: number; // 盈亏比例
}
/**
* 概念/行业板块
*/
export interface Sector {
id: string; // 板块 ID
name: string; // 板块名称
type: 'industry' | 'concept'; // 类型(行业/概念)
changePercent: number; // 板块涨跌幅
leadingStock?: string; // 领涨股
stockCount?: number; // 成分股数量
description?: string; // 描述
}
/**
* 股票搜索结果
*/
export interface StockSearchResult {
code: string;
name: string;
market: string;
industry?: string;
}

108
src/types/user.ts Normal file
View File

@@ -0,0 +1,108 @@
/**
* 用户相关类型定义
*
* 包含用户信息、认证、权限等类型
*/
/**
* 用户基础信息
*/
export interface UserInfo {
id: string | number; // 用户 ID
username: string; // 用户名
email?: string; // 邮箱
phone?: string; // 手机号
avatar?: string; // 头像 URL
nickname?: string; // 昵称
realName?: string; // 真实姓名
gender?: 'male' | 'female' | 'other'; // 性别
birthday?: string; // 生日
createdAt?: string; // 注册时间
updatedAt?: string; // 更新时间
}
/**
* 用户认证信息
*/
export interface AuthInfo {
token: string; // 访问令牌
refreshToken?: string; // 刷新令牌
expiresIn?: number; // 过期时间(秒)
tokenType?: string; // 令牌类型(如 'Bearer'
}
/**
* 登录请求参数
*/
export interface LoginParams {
username: string; // 用户名或手机号
password: string; // 密码
captcha?: string; // 验证码
remember?: boolean; // 记住登录状态
}
/**
* 注册请求参数
*/
export interface RegisterParams {
username: string; // 用户名
password: string; // 密码
confirmPassword?: string; // 确认密码
phone?: string; // 手机号
email?: string; // 邮箱
verificationCode?: string; // 短信验证码
agreeTerm?: boolean; // 是否同意条款
}
/**
* 用户权限
*/
export interface UserPermissions {
roles: string[]; // 角色列表
permissions: string[]; // 权限列表
}
/**
* 用户订阅信息
*/
export interface UserSubscription {
userId: string | number; // 用户 ID
plan: 'free' | 'basic' | 'pro' | 'enterprise'; // 订阅套餐
status: 'active' | 'expired' | 'cancelled'; // 状态
startDate: string; // 开始日期
endDate: string; // 结束日期
autoRenew?: boolean; // 是否自动续费
features?: string[]; // 可用功能列表
}
/**
* 用户资金账户
*/
export interface UserAccount {
userId: string | number; // 用户 ID
balance: number; // 可用余额
frozenBalance: number; // 冻结资金
totalAssets: number; // 总资产
marketValue: number; // 持仓市值
profit: number; // 盈亏金额
profitPercent: number; // 盈亏比例
currency: string; // 币种(默认 'CNY'
}
/**
* 用户设置
*/
export interface UserSettings {
userId: string | number; // 用户 ID
theme?: 'light' | 'dark'; // 主题
language?: 'zh-CN' | 'en-US'; // 语言
notifications?: {
email: boolean; // 邮件通知
sms: boolean; // 短信通知
push: boolean; // 推送通知
};
privacy?: {
showProfile: boolean; // 显示个人资料
showPositions: boolean; // 显示持仓
};
}

90
src/utils/debounce.ts Normal file
View File

@@ -0,0 +1,90 @@
// src/utils/debounce.ts
// 防抖工具函数TypeScript 版本)
/**
* 防抖函数返回类型
* @template T - 原函数类型
*/
export interface DebouncedFunction<T extends (...args: any[]) => any> {
/**
* 执行防抖后的函数
* @param args - 原函数的参数
*/
(...args: Parameters<T>): void;
/**
* 取消待执行的函数调用
*/
cancel: () => void;
}
/**
* 防抖函数 - 延迟执行,短时间内多次调用只执行最后一次
*
* 工作原理:
* 1. 调用防抖函数时,清除之前的定时器
* 2. 设置新的定时器,延迟 delay 毫秒后执行
* 3. 如果在延迟期间再次调用,重复步骤 1-2
* 4. 只有最后一次调用会在延迟后实际执行
*
* 使用场景:
* - 搜索框输入:用户停止输入后才发送请求
* - 窗口 resize窗口调整结束后才重新计算布局
* - Socket 事件:短时间内收到多个事件,只处理最后一个
*
* @template T - 函数类型(泛型约束:任意函数)
* @param {T} func - 要防抖的函数
* @param {number} delay - 延迟时间(毫秒)
* @returns {DebouncedFunction<T>} 防抖后的函数(带 cancel 方法)
*
* @example
* ```typescript
* // 示例 1无参数函数
* const debouncedSave = debounce(() => {
* console.log('保存数据');
* }, 1000);
*
* debouncedSave(); // 1秒后执行
* debouncedSave(); // 取消上次,重新计时 1 秒
* debouncedSave.cancel(); // 取消执行
*
* // 示例 2带参数函数
* const debouncedSearch = debounce((keyword: string) => {
* console.log('搜索:', keyword);
* }, 500);
*
* debouncedSearch('react'); // 500ms 后执行
* debouncedSearch('redux'); // 取消上次,重新计时
* ```
*/
export function debounce<T extends (...args: any[]) => any>(
func: T,
delay: number
): DebouncedFunction<T> {
// 使用 NodeJS.Timeout 类型(支持浏览器和 Node 环境)
let timerId: ReturnType<typeof setTimeout> | null = null;
// 防抖函数主体
const debouncedFn = (...args: Parameters<T>): void => {
// 清除之前的定时器(防抖核心逻辑)
if (timerId !== null) {
clearTimeout(timerId);
}
// 设置新的定时器
timerId = setTimeout(() => {
func(...args);
timerId = null; // 执行后重置定时器 ID
}, delay);
};
// 添加 cancel 方法(用于组件卸载时清理)
debouncedFn.cancel = (): void => {
if (timerId !== null) {
clearTimeout(timerId);
timerId = null;
}
};
return debouncedFn;
}

View File

@@ -1,7 +1,7 @@
// src/views/Community/components/DynamicNewsCard.js // src/views/Community/components/DynamicNewsCard.js
// 横向滚动事件卡片组件(实时要闻·动态追踪) // 横向滚动事件卡片组件(实时要闻·动态追踪)
import React, { forwardRef, useState, useEffect, useMemo, useCallback, useRef } from 'react'; import React, { forwardRef, useState, useEffect, useMemo, useCallback, useRef, useImperativeHandle } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { import {
Card, Card,
@@ -44,8 +44,9 @@ import {
selectFourRowEventsWithLoading selectFourRowEventsWithLoading
} from '../../../store/slices/communityDataSlice'; } from '../../../store/slices/communityDataSlice';
import { usePagination } from './DynamicNewsCard/hooks/usePagination'; import { usePagination } from './DynamicNewsCard/hooks/usePagination';
import { PAGINATION_CONFIG, DISPLAY_MODES } from './DynamicNewsCard/constants'; import { PAGINATION_CONFIG, DISPLAY_MODES, REFRESH_DEBOUNCE_DELAY } from './DynamicNewsCard/constants';
import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme'; import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme';
import { debounce } from '../../../utils/debounce';
// 🔍 调试:渲染计数器 // 🔍 调试:渲染计数器
let dynamicNewsCardRenderCount = 0; let dynamicNewsCardRenderCount = 0;
@@ -84,6 +85,7 @@ const DynamicNewsCard = forwardRef(({
// Refs // Refs
const cardHeaderRef = useRef(null); const cardHeaderRef = useRef(null);
const cardBodyRef = useRef(null); const cardBodyRef = useRef(null);
const virtualizedGridRef = useRef(null); // ⚡ VirtualizedFourRowGrid 的 ref用于获取滚动位置
// 从 Redux 读取关注状态 // 从 Redux 读取关注状态
const eventFollowStatus = useSelector(selectEventFollowStatus); const eventFollowStatus = useSelector(selectEventFollowStatus);
@@ -208,6 +210,124 @@ const [currentMode, setCurrentMode] = useState('vertical');
setCurrentMode(mode); setCurrentMode(mode);
}, [mode]); }, [mode]);
/**
* ⚡【核心逻辑】执行刷新的回调函数(包含原有的智能刷新逻辑)
*
* 此函数会被 debounce 包装,避免短时间内频繁刷新
*/
const executeRefresh = useCallback(() => {
const state = {
mode,
currentPage: pagination?.current_page || 1,
};
console.log('[DynamicNewsCard] ⏰ executeRefresh() 执行(防抖延迟后)', state);
if (mode === 'vertical') {
// ========== 纵向模式 ==========
// 只在第1页时刷新避免打断用户浏览其他页
if (state.currentPage === 1) {
console.log('[DynamicNewsCard] 纵向模式 + 第1页 → 刷新列表');
handlePageChange(1); // 清空缓存并刷新第1页
toast({
title: '检测到新事件',
status: 'info',
duration: 2000,
isClosable: true,
});
} else {
console.log(`[DynamicNewsCard] 纵向模式 + 第${state.currentPage}页 → 不刷新(避免打断用户)`);
}
} else if (mode === 'four-row') {
// ========== 平铺模式 ==========
// 检查滚动位置,只有在顶部时才刷新
const scrollPos = virtualizedGridRef.current?.getScrollPosition();
if (scrollPos?.isNearTop) {
// 用户在顶部 10% 区域,安全刷新
console.log('[DynamicNewsCard] 平铺模式 + 滚动在顶部 → 刷新列表');
handlePageChange(1); // 清空并刷新
toast({
title: '检测到新事件,已刷新',
status: 'info',
duration: 2000,
isClosable: true,
});
} else {
// 用户不在顶部,显示提示但不自动刷新
console.log('[DynamicNewsCard] 平铺模式 + 滚动不在顶部 → 仅提示,不刷新');
toast({
title: '有新事件发布',
description: '滚动到顶部查看',
status: 'info',
duration: 3000,
isClosable: true,
});
}
}
}, [mode, pagination, handlePageChange, toast]);
/**
* ⚡【防抖包装】创建防抖版本的刷新函数
*
* 使用 useMemo 确保防抖函数在 executeRefresh 不变时保持引用稳定
* 防抖延迟REFRESH_DEBOUNCE_DELAY (2000ms)
*
* 效果:短时间内收到多个新事件,只执行最后一次刷新
*/
const debouncedRefresh = useMemo(
() => debounce(executeRefresh, REFRESH_DEBOUNCE_DELAY),
[executeRefresh]
);
/**
* ⚡ 暴露方法给父组件(用于 Socket 自动刷新)
*/
useImperativeHandle(ref, () => ({
/**
* 智能刷新方法(带防抖,避免频繁刷新)
*
* 调用此方法时:
* 1. 清除之前的定时器(如果有)
* 2. 设置新的定时器(延迟 REFRESH_DEBOUNCE_DELAY 后执行)
* 3. 如果在延迟期间再次调用,重复步骤 1-2
* 4. 只有最后一次调用会在延迟后实际执行 executeRefresh()
*/
refresh: () => {
console.log('[DynamicNewsCard] 🔔 refresh() 被调用(设置防抖定时器)', {
mode,
currentPage: pagination?.current_page || 1,
debounceDelay: `${REFRESH_DEBOUNCE_DELAY}ms`,
});
// 调用防抖包装后的函数
debouncedRefresh();
},
/**
* 获取当前状态(用于调试)
*/
getState: () => ({
mode,
currentPage: pagination?.current_page || 1,
totalPages: pagination?.total_pages || 1,
total: pagination?.total || 0,
loading,
}),
}), [mode, pagination, loading, debouncedRefresh]);
/**
* ⚡【清理逻辑】组件卸载时取消待执行的防抖函数
*
* 作用:避免组件卸载后仍然执行刷新操作(防止内存泄漏和潜在错误)
*/
useEffect(() => {
return () => {
console.log('[DynamicNewsCard] 🧹 组件卸载,取消待执行的防抖刷新');
debouncedRefresh.cancel();
};
}, [debouncedRefresh]);
// 监听 error 状态,显示空数据提示 // 监听 error 状态,显示空数据提示
useEffect(() => { useEffect(() => {
if (error && error.includes('暂无更多数据')) { if (error && error.includes('暂无更多数据')) {
@@ -578,6 +698,7 @@ const [currentMode, setCurrentMode] = useState('vertical');
eventFollowStatus={eventFollowStatus} eventFollowStatus={eventFollowStatus}
onToggleFollow={handleToggleFollow} onToggleFollow={handleToggleFollow}
hasMore={hasMore} hasMore={hasMore}
virtualizedGridRef={virtualizedGridRef} // ⚡ 传递 ref 给 VirtualizedFourRowGrid
/> />
</Box> </Box>
</CardBody> </CardBody>

View File

@@ -28,6 +28,7 @@ import VerticalModeLayout from './VerticalModeLayout';
* @param {boolean} hasMore - 是否还有更多数据 * @param {boolean} hasMore - 是否还有更多数据
* @param {Object} eventFollowStatus - 事件关注状态 { [eventId]: { isFollowing, followerCount } } * @param {Object} eventFollowStatus - 事件关注状态 { [eventId]: { isFollowing, followerCount } }
* @param {Function} onToggleFollow - 关注按钮回调 * @param {Function} onToggleFollow - 关注按钮回调
* @param {React.Ref} virtualizedGridRef - VirtualizedFourRowGrid 的 ref用于获取滚动位置
*/ */
const EventScrollList = ({ const EventScrollList = ({
events, events,
@@ -46,7 +47,8 @@ const EventScrollList = ({
mode = 'vertical', mode = 'vertical',
hasMore = true, hasMore = true,
eventFollowStatus = {}, eventFollowStatus = {},
onToggleFollow onToggleFollow,
virtualizedGridRef
}) => { }) => {
const scrollContainerRef = useRef(null); const scrollContainerRef = useRef(null);
@@ -111,6 +113,7 @@ const EventScrollList = ({
> >
{/* 平铺网格模式 - 使用虚拟滚动 + 双向无限滚动 */} {/* 平铺网格模式 - 使用虚拟滚动 + 双向无限滚动 */}
<VirtualizedFourRowGrid <VirtualizedFourRowGrid
ref={virtualizedGridRef} // ⚡ 传递 ref用于获取滚动位置
display={mode === 'four-row' ? 'block' : 'none'} display={mode === 'four-row' ? 'block' : 'none'}
columnsPerRow={4} // 每行显示4列 columnsPerRow={4} // 每行显示4列
events={displayEvents || events} // 使用累积列表(如果有) events={displayEvents || events} // 使用累积列表(如果有)

View File

@@ -1,7 +1,7 @@
// src/views/Community/components/DynamicNewsCard/VirtualizedFourRowGrid.js // src/views/Community/components/DynamicNewsCard/VirtualizedFourRowGrid.js
// 虚拟化网格组件(支持多列布局 + 纵向滚动 + 无限滚动) // 虚拟化网格组件(支持多列布局 + 纵向滚动 + 无限滚动)
import React, { useRef, useMemo, useEffect } from 'react'; import React, { useRef, useMemo, useEffect, forwardRef, useImperativeHandle } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import { Box, Grid, Spinner, Text, VStack, Center, HStack, IconButton, useBreakpointValue } from '@chakra-ui/react'; import { Box, Grid, Spinner, Text, VStack, Center, HStack, IconButton, useBreakpointValue } from '@chakra-ui/react';
import { RepeatIcon } from '@chakra-ui/icons'; import { RepeatIcon } from '@chakra-ui/icons';
@@ -25,7 +25,7 @@ import DynamicNewsEventCard from '../EventCard/DynamicNewsEventCard';
* @param {boolean} props.hasMore - 是否还有更多数据 * @param {boolean} props.hasMore - 是否还有更多数据
* @param {boolean} props.loading - 加载状态 * @param {boolean} props.loading - 加载状态
*/ */
const VirtualizedFourRowGrid = ({ const VirtualizedFourRowGrid = forwardRef(({
display = 'block', display = 'block',
events, events,
columnsPerRow = 4, columnsPerRow = 4,
@@ -42,7 +42,7 @@ const VirtualizedFourRowGrid = ({
loading, loading,
error, // 新增:错误状态 error, // 新增:错误状态
onRetry, // 新增:重试回调 onRetry, // 新增:重试回调
}) => { }, ref) => {
const parentRef = useRef(null); const parentRef = useRef(null);
const isLoadingMore = useRef(false); // 防止重复加载 const isLoadingMore = useRef(false); // 防止重复加载
const lastRefreshTime = useRef(0); // 记录上次刷新时间用于30秒防抖 const lastRefreshTime = useRef(0); // 记录上次刷新时间用于30秒防抖
@@ -81,6 +81,31 @@ const VirtualizedFourRowGrid = ({
overscan: 2, // 预加载2行上下各1行 overscan: 2, // 预加载2行上下各1行
}); });
/**
* ⚡ 暴露方法给父组件(用于 Socket 刷新判断)
*/
useImperativeHandle(ref, () => ({
/**
* 获取当前滚动位置信息
* @returns {Object|null} 滚动位置信息
*/
getScrollPosition: () => {
const scrollElement = parentRef.current;
if (!scrollElement) return null;
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
const isNearTop = scrollTop < clientHeight * 0.1; // 顶部 10% 区域
return {
scrollTop,
scrollHeight,
clientHeight,
isNearTop,
scrollPercentage: ((scrollTop + clientHeight) / scrollHeight) * 100,
};
},
}), []);
/** /**
* 【核心逻辑1】无限滚动 + 顶部刷新 - 监听滚动事件,根据滚动位置自动加载数据或刷新 * 【核心逻辑1】无限滚动 + 顶部刷新 - 监听滚动事件,根据滚动位置自动加载数据或刷新
* *
@@ -360,6 +385,8 @@ const VirtualizedFourRowGrid = ({
</Box> </Box>
</Box> </Box>
); );
}; });
VirtualizedFourRowGrid.displayName = 'VirtualizedFourRowGrid';
export default VirtualizedFourRowGrid; export default VirtualizedFourRowGrid;

View File

@@ -38,3 +38,21 @@ export const TOAST_CONFIG = {
DURATION_ERROR: 3000, // 错误提示持续时间(毫秒) DURATION_ERROR: 3000, // 错误提示持续时间(毫秒)
DURATION_WARNING: 2000, // 警告提示持续时间(毫秒) DURATION_WARNING: 2000, // 警告提示持续时间(毫秒)
}; };
// ========== Socket 刷新防抖配置 ==========
/**
* Socket 新事件刷新防抖延迟(毫秒)
*
* 作用:避免短时间内收到多个新事件时频繁刷新列表
*
* 场景示例:
* - 第 1 秒:收到新事件 → 延迟 2 秒刷新
* - 第 2 秒:收到新事件 → 取消上次,重新延迟 2 秒
* - 第 3 秒:收到新事件 → 取消上次,重新延迟 2 秒
* - 第 5 秒:触发刷新 → 只发送 1 次 API 请求
*
* 推荐值2000ms (2 秒)
* - 太短(如 500ms→ 仍可能触发多次刷新
* - 太长(如 5000ms→ 用户感知延迟过高
*/
export const REFRESH_DEBOUNCE_DELAY = 2000;

View File

@@ -4,6 +4,7 @@
import React, { useState, useCallback, useEffect } from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { import {
Box,
Card, Card,
CardBody, CardBody,
VStack, VStack,
@@ -32,6 +33,7 @@ import TransmissionChainAnalysis from '../../../EventDetail/components/Transmiss
import SubscriptionBadge from '../../../../components/SubscriptionBadge'; import SubscriptionBadge from '../../../../components/SubscriptionBadge';
import SubscriptionUpgradeModal from '../../../../components/SubscriptionUpgradeModal'; import SubscriptionUpgradeModal from '../../../../components/SubscriptionUpgradeModal';
import { PROFESSIONAL_COLORS } from '../../../../constants/professionalTheme'; import { PROFESSIONAL_COLORS } from '../../../../constants/professionalTheme';
import EventCommentSection from '../../../../components/EventCommentSection';
/** /**
* 动态新闻详情面板主组件 * 动态新闻详情面板主组件
@@ -414,6 +416,11 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
eventService={eventService} eventService={eventService}
/> />
</CollapsibleSection> </CollapsibleSection>
{/* 讨论区(评论区) - 所有登录用户可用 */}
<Box mt={4}>
<EventCommentSection eventId={event.id} />
</Box>
</VStack> </VStack>
</CardBody> </CardBody>

View File

@@ -74,7 +74,9 @@ const KeywordsCarousel = ({
whiteSpace="nowrap" whiteSpace="nowrap"
textShadow="0 0 20px rgba(255, 195, 0, 0.3)" textShadow="0 0 20px rgba(255, 195, 0, 0.3)"
> >
{currentKeyword} {typeof currentKeyword === 'string'
? currentKeyword
: currentKeyword?.concept || '未知标签'}
</Text> </Text>
</Box> </Box>
</Tooltip> </Tooltip>

View File

@@ -1,6 +1,6 @@
// src/views/Community/index.js // src/views/Community/index.js
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { import {
fetchPopularKeywords, fetchPopularKeywords,
@@ -40,6 +40,7 @@ import { PROFESSIONAL_COLORS } from '../../constants/professionalTheme';
const Community = () => { const Community = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); // ⚡ 获取当前路由信息(用于判断是否在 /community 页面)
const dispatch = useDispatch(); const dispatch = useDispatch();
// Redux状态 // Redux状态
@@ -71,7 +72,10 @@ const Community = () => {
}); });
// ⚡ 通知权限引导 // ⚡ 通知权限引导
const { browserPermission, requestBrowserPermission } = useNotification(); const { browserPermission, requestBrowserPermission, registerEventUpdateCallback } = useNotification();
// ⚡ DynamicNewsCard 的 ref用于触发刷新
const dynamicNewsCardRef = useRef(null);
// 通知横幅显示状态 // 通知横幅显示状态
const [showNotificationBanner, setShowNotificationBanner] = useState(false); const [showNotificationBanner, setShowNotificationBanner] = useState(false);
@@ -160,6 +164,63 @@ const Community = () => {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, []); // 空依赖数组,只在组件挂载时执行一次 }, []); // 空依赖数组,只在组件挂载时执行一次
/**
* ⚡ 【核心逻辑】注册 Socket 新事件回调 - 当收到新事件时智能刷新列表
*
* 工作流程:
* 1. Socket 收到 'new_event' 事件 → NotificationContext 触发所有注册的回调
* 2. 本回调被触发 → 检查当前路由是否为 /community
* 3. 如果在 /community 页面 → 调用 DynamicNewsCard.refresh() 方法
* 4. DynamicNewsCard 根据模式和滚动位置决定是否刷新:
* - 纵向模式 + 第1页 → 刷新列表
* - 纵向模式 + 其他页 → 不刷新(避免打断用户)
* - 平铺模式 + 滚动在顶部 → 刷新列表
* - 平铺模式 + 滚动不在顶部 → 仅显示 Toast 提示
*
* 设计要点:
* - 使用 registerEventUpdateCallback 注册回调,返回的函数用于清理
* - 路由检查:只在 /community 页面触发刷新
* - 智能刷新:由 DynamicNewsCard 根据上下文决定刷新策略
* - 自动清理:组件卸载时自动注销回调
*/
useEffect(() => {
// 定义回调函数
const handleNewEvent = (eventData) => {
console.log('[Community] 🔔 收到新事件通知', {
currentPath: location.pathname,
eventData,
});
// 检查是否在 /community 页面
if (location.pathname === '/community') {
console.log('[Community] ✅ 当前在事件中心页面,触发 DynamicNewsCard 刷新');
// 调用 DynamicNewsCard 的 refresh 方法(智能刷新)
if (dynamicNewsCardRef.current) {
dynamicNewsCardRef.current.refresh();
} else {
console.warn('[Community] ⚠️ DynamicNewsCard ref 不可用,无法触发刷新');
}
} else {
console.log('[Community] ⏭️ 当前不在事件中心页面,跳过刷新', {
currentPath: location.pathname,
});
}
};
// 注册回调(返回清理函数)
const unregister = registerEventUpdateCallback(handleNewEvent);
console.log('[Community] ✅ 已注册 Socket 事件更新回调');
// 组件卸载时清理
return () => {
if (unregister) {
unregister();
console.log('[Community] 🧹 已注销 Socket 事件更新回调');
}
};
}, [location.pathname, registerEventUpdateCallback]); // 依赖路由变化重新注册
return ( return (
<Box minH="100vh" bg={bgColor}> <Box minH="100vh" bg={bgColor}>
{/* 主内容区域 */} {/* 主内容区域 */}
@@ -206,6 +267,7 @@ const Community = () => {
{/* 实时要闻·动态追踪 - 横向滚动 */} {/* 实时要闻·动态追踪 - 横向滚动 */}
<DynamicNewsCard <DynamicNewsCard
ref={dynamicNewsCardRef} // ⚡ 传递 ref用于触发刷新
filters={filters} filters={filters}
popularKeywords={popularKeywords} popularKeywords={popularKeywords}
lastUpdateTime={lastUpdateTime} lastUpdateTime={lastUpdateTime}

81
tsconfig.json Normal file
View File

@@ -0,0 +1,81 @@
{
"compilerOptions": {
/* */
"target": "ES2020", // 编译目标(支持现代浏览器)
"lib": ["DOM", "DOM.Iterable", "ES2020"], // 类型库
"jsx": "react-jsx", // JSX 编译模式React 17+ 新 JSX 转换)
"module": "ESNext", // 模块系统
"moduleResolution": "node", // 模块解析策略
/* - */
"strict": false, // 关闭全部严格检查(初期)
"noImplicitAny": true, // 禁止隐式 any推荐启用
"strictNullChecks": false, // 暂不启用空值检查(后续开启)
"strictFunctionTypes": false, // 暂不启用函数类型严格检查
"strictBindCallApply": false, // 暂不启用 bind/call/apply 严格检查
"strictPropertyInitialization": false, // 暂不启用属性初始化检查
"noImplicitThis": true, // 禁止隐式 this
"alwaysStrict": true, // 总是以严格模式解析
/* */
"noUnusedLocals": false, // 初期不检查未使用的局部变量
"noUnusedParameters": false, // 初期不检查未使用的参数
"noImplicitReturns": false, // 初期不强制所有路径都返回值
"noFallthroughCasesInSwitch": true, // 检查 switch 语句穿透
/* - JS/TS */
"allowJs": true, // 允许编译 JS 文件
"checkJs": false, // 不检查 JS 文件(避免大量错误)
"isolatedModules": true, // 确保每个文件都可以独立编译
"esModuleInterop": true, // ES 模块互操作性
"allowSyntheticDefaultImports": true, // 允许默认导入
"skipLibCheck": true, // 跳过库文件类型检查(加快编译)
"forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
"resolveJsonModule": true, // 支持导入 JSON 文件
/* */
"noEmit": true, // 不生成输出文件(由 Babel/Webpack 处理)
"declaration": false, // 不生成类型声明文件
"sourceMap": false, // 不生成 source map由 Webpack 处理)
/* - craco.config.js */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@assets/*": ["src/assets/*"],
"@components/*": ["src/components/*"],
"@constants/*": ["src/constants/*"],
"@contexts/*": ["src/contexts/*"],
"@data/*": ["src/data/*"],
"@hooks/*": ["src/hooks/*"],
"@layouts/*": ["src/layouts/*"],
"@lib/*": ["src/lib/*"],
"@mocks/*": ["src/mocks/*"],
"@providers/*": ["src/providers/*"],
"@routes/*": ["src/routes/*"],
"@services/*": ["src/services/*"],
"@store/*": ["src/store/*"],
"@styles/*": ["src/styles/*"],
"@theme/*": ["src/theme/*"],
"@utils/*": ["src/utils/*"],
"@variables/*": ["src/variables/*"],
"@views/*": ["src/views/*"]
}
},
/* / */
"include": [
"src/**/*", // 包含 src 目录下所有 .ts, .tsx, .js, .jsx 文件
"src/**/*.json" // 包含 JSON 文件
],
"exclude": [
"node_modules", // 排除依赖
"build", // 排除构建输出
"dist", // 排除打包输出
"scripts", // 排除脚本
"**/*.spec.ts", // 排除测试文件(可选)
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx"
]
}