Compare commits

...

11 Commits

Author SHA1 Message Date
zdl
20994cfb13 Merge branch 'feature_bugfix/251201_vf_h5_ui' into feature_bugfix/251201_py_h5_ui
* feature_bugfix/251201_vf_h5_ui:
  fix: 事件详情唔错页面UI调整
  fix: 调整事件详情页面
  feat: 事件详情页 URL ID 加密,防止用户遍历
  style: 首页整体尺寸缩小约 67%
  fix: 调整客服弹窗 将 PC 端聊天窗口从 380×640 调整为 450×750。 H5 端:宽度占满,高度根据宽度等比缩放
  fix: ICP 备案号现在可以点击跳转到 https://beian.miit.gov.cn/
  feat: 田间mock数据
  feat: 个股中心复用 TradeDatePicker 日期选择器组件
  feat: 概念中心历史时间轴弹窗UI调整
  feat: 提取日历选择器组件
  refactor: 提取 ConceptStocksModal 为通用组件,统一概念中心和个股中心弹窗
  refactor: 事件详情弹窗改用 Drawer 组件从底部弹出
  fix: 在 viewport meta 标签中添加了 viewport-fit=cover,这样浏览器会将页面内容延伸到曲面屏边缘,同时启用 safe-area-inset-* CSS 环境变量 在普通设备上保持至少 16px 的右侧内边距 在华为 MATE70 PRO 等曲面屏设备上,使用系统提供的安全区域值,避免右侧导航被遮挡
  fix: 概念中心H5端卡片尺寸优化,一屏可显示更多内容
  fix: 修复自选股添加失败 405 错误
  fix: H5端热门事件移除Tooltip避免黑色悬浮框无法消除
2025-12-05 09:42:52 +08:00
zdl
bad5290fe2 Merge branch 'feature_bugfix/251201_vf_h5_ui' into feature_bugfix/251201_py_h5_ui
* feature_bugfix/251201_vf_h5_ui:
  feat: 日k 和 分时h5UI调整
  fix: 弹窗固定高度
  feat: K线添加mock数据
  feat: 添加批量获取K线数据的 mock handler
2025-12-04 14:12:10 +08:00
zdl
daee0427e4 fix: 修复 useWatchlist.js 合并冲突遗留问题
- 移除重复的 handleRemoveFromWatchlist 导出
- 移除 JSDoc 中重复的类型声明
- 清理残留的错误注释

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 13:35:51 +08:00
zdl
2a653afea1 Merge branch 'feature_bugfix/251201_vf_h5_ui' into feature_bugfix/251201_py_h5_ui
* feature_bugfix/251201_vf_h5_ui:
  fix: 导航效果UI修复
  feat: 个股添加个股列表弹窗
  fix: 概念中心UI
  fix: 个股中心页面日期数据源统一
  fix: 修改的后端代码 /api/market/statistics 接口 添加日期格式化逻辑 //api/concepts/daily-top 添加日期格式化逻辑 /api/market/heatmap 接口 已经有正确的格式化
2025-12-04 11:53:37 +08:00
a37206ec97 update pay ui 2025-12-04 10:58:30 +08:00
5e5e2160b0 update pay ui 2025-12-04 10:43:17 +08:00
zdl
e8285599e8 Merge branch 'feature_bugfix/251201_vf_h5_ui' into feature_bugfix/251201_py_h5_ui
* feature_bugfix/251201_vf_h5_ui:
  fix: 去除个股中心动画,添加mock数据
  feat: 首页代码优化
2025-12-03 18:31:27 +08:00
0eb760fa31 update pay ui 2025-12-03 17:40:57 +08:00
805b897afa update pay ui 2025-12-03 17:13:49 +08:00
2988af9806 Merge branch 'feature_bugfix/251201_py_h5_ui' of https://git.valuefrontier.cn/vf/vf_react into feature_bugfix/251201_py_h5_ui 2025-12-03 16:50:45 +08:00
63023adcf3 update pay ui 2025-12-03 16:50:39 +08:00
4 changed files with 154 additions and 49 deletions

View File

@@ -1999,7 +1999,7 @@ class MCPAgentIntegrated:
model=self.kimi_model,
messages=messages,
temperature=1.0, # Kimi 推荐
max_tokens=8192, # 足够容纳 reasoning_content
max_tokens=128000, # 足够容纳 reasoning_content
)
choice = response.choices[0]
@@ -2074,7 +2074,7 @@ class MCPAgentIntegrated:
model=self.deepmoney_model,
messages=messages,
temperature=0.7,
max_tokens=8192,
max_tokens=32784,
)
summary = response.choices[0].message.content
@@ -2268,7 +2268,7 @@ class MCPAgentIntegrated:
model="kimi-k2-turbo-preview", # 使用非思考模型,更快
messages=messages,
temperature=0.7,
max_tokens=8192, # 增加 token 限制以支持图表配置
max_tokens=128000, # 增加 token 限制以支持图表配置
)
summary = response.choices[0].message.content
@@ -2355,7 +2355,7 @@ class MCPAgentIntegrated:
model=self.deepmoney_model,
messages=messages,
temperature=0.3,
max_tokens=4096,
max_tokens=32768,
)
title = response.choices[0].message.content.strip()
@@ -2450,7 +2450,7 @@ class MCPAgentIntegrated:
model=planning_model,
messages=messages,
temperature=1.0,
max_tokens=8192,
max_tokens=32768,
stream=True, # 启用流式输出
)
@@ -2494,7 +2494,7 @@ class MCPAgentIntegrated:
model=self.deepmoney_model,
messages=messages,
temperature=0.7,
max_tokens=8192,
max_tokens=32768,
)
plan_content = fallback_response.choices[0].message.content
@@ -2690,7 +2690,7 @@ class MCPAgentIntegrated:
model="kimi-k2-turbo-preview",
messages=messages,
temperature=0.7,
max_tokens=8192,
max_tokens=32768,
stream=True, # 启用流式输出
)
@@ -2724,7 +2724,7 @@ class MCPAgentIntegrated:
model=self.deepmoney_model,
messages=messages,
temperature=0.7,
max_tokens=8192,
max_tokens=32768,
)
final_summary = fallback_response.choices[0].message.content
@@ -3676,7 +3676,7 @@ async def stream_role_response(
tool_choice="auto",
stream=False, # 工具调用不使用流式
temperature=0.7,
max_tokens=8192, # 增大 token 限制以避免输出被截断
max_tokens=32768, # 增大 token 限制以避免输出被截断
)
assistant_message = response.choices[0].message

View File

@@ -1,16 +1,19 @@
// src/hooks/useWatchlist.js
// 自选股管理自定义 Hook
// 自选股管理自定义 Hook(导航栏专用,与 Redux 状态同步)
import { useState, useCallback } from 'react';
import { useState, useCallback, useEffect, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useToast } from '@chakra-ui/react';
import { logger } from '../utils/logger';
import { getApiBase } from '../utils/apiConfig';
import { toggleWatchlist as toggleWatchlistAction, loadWatchlist } from '../store/slices/stockSlice';
const WATCHLIST_PAGE_SIZE = 10;
/**
* 自选股管理 Hook
* 提供自选股加载、分页、添加、移除等功能
* 自选股管理 Hook(导航栏专用)
* 提供自选股加载、分页、移除等功能
* 监听 Redux 中的 watchlist 变化,自动刷新行情数据
*
* @returns {{
* watchlistQuotes: Array,
@@ -19,6 +22,7 @@ const WATCHLIST_PAGE_SIZE = 10;
* setWatchlistPage: Function,
* WATCHLIST_PAGE_SIZE: number,
* loadWatchlistQuotes: Function,
* followingEvents: Array,
* handleAddToWatchlist: Function,
* handleRemoveFromWatchlist: Function,
* isInWatchlist: Function
@@ -26,9 +30,31 @@ const WATCHLIST_PAGE_SIZE = 10;
*/
export const useWatchlist = () => {
const toast = useToast();
const dispatch = useDispatch();
const [watchlistQuotes, setWatchlistQuotes] = useState([]);
const [watchlistLoading, setWatchlistLoading] = useState(false);
const [watchlistPage, setWatchlistPage] = useState(1);
const [followingEvents, setFollowingEvents] = useState([]);
// 从 Redux 获取自选股列表长度(用于监听变化)
// 使用 length 作为依赖,避免数组引用变化导致不必要的重新渲染
const reduxWatchlistLength = useSelector(state => state.stock.watchlist?.length || 0);
// 检查 Redux watchlist 是否已初始化(加载状态)
const reduxWatchlistLoading = useSelector(state => state.stock.loading?.watchlist);
// 用于跟踪上一次的 watchlist 长度
const prevWatchlistLengthRef = useRef(-1); // 初始设为 -1确保第一次变化也能检测到
// 初始化时加载 Redux watchlist确保 Redux 状态被初始化)
const hasInitializedRef = useRef(false);
useEffect(() => {
if (!hasInitializedRef.current) {
hasInitializedRef.current = true;
logger.debug('useWatchlist', '初始化 Redux watchlist');
dispatch(loadWatchlist());
}
}, [dispatch]);
// 加载自选股实时行情
const loadWatchlistQuotes = useCallback(async () => {
@@ -44,6 +70,7 @@ export const useWatchlist = () => {
const data = await resp.json();
if (data && data.success && Array.isArray(data.data)) {
setWatchlistQuotes(data.data);
logger.debug('useWatchlist', '自选股行情加载成功', { count: data.data.length });
} else {
setWatchlistQuotes([]);
}
@@ -60,6 +87,33 @@ export const useWatchlist = () => {
}
}, []);
// 监听 Redux watchlist 长度变化,自动刷新行情数据
useEffect(() => {
const currentLength = reduxWatchlistLength;
const prevLength = prevWatchlistLengthRef.current;
// 只有当 watchlist 长度发生变化时才刷新
// prevLength = -1 表示初始状态,此时不触发刷新(由菜单打开时触发)
if (prevLength !== -1 && currentLength !== prevLength) {
logger.debug('useWatchlist', 'Redux watchlist 长度变化,刷新行情', {
prevLength,
currentLength
});
// 延迟一小段时间再刷新,确保后端数据已更新
const timer = setTimeout(() => {
logger.debug('useWatchlist', '执行 loadWatchlistQuotes');
loadWatchlistQuotes();
}, 500);
prevWatchlistLengthRef.current = currentLength;
return () => clearTimeout(timer);
}
// 更新 ref
prevWatchlistLengthRef.current = currentLength;
}, [reduxWatchlistLength, loadWatchlistQuotes]);
// 添加到自选股
const handleAddToWatchlist = useCallback(async (stockCode, stockName) => {
try {
@@ -89,33 +143,42 @@ export const useWatchlist = () => {
// 从自选股移除
const handleRemoveFromWatchlist = useCallback(async (stockCode) => {
try {
const base = getApiBase();
const resp = await fetch(base + `/api/account/watchlist/${stockCode}`, {
method: 'DELETE',
credentials: 'include'
// 找到股票名称
const stockItem = watchlistQuotes.find(item => {
const normalize6 = (code) => {
const m = String(code || '').match(/(\d{6})/);
return m ? m[1] : String(code || '');
};
return normalize6(item.stock_code) === normalize6(stockCode);
});
const data = await resp.json().catch(() => ({}));
if (resp.ok && data && data.success !== false) {
setWatchlistQuotes((prev) => {
const normalize6 = (code) => {
const m = String(code || '').match(/(\d{6})/);
return m ? m[1] : String(code || '');
};
const target = normalize6(stockCode);
const updated = (prev || []).filter((x) => normalize6(x.stock_code) !== target);
const newMaxPage = Math.max(1, Math.ceil((updated.length || 0) / WATCHLIST_PAGE_SIZE));
setWatchlistPage((p) => Math.min(p, newMaxPage));
return updated;
});
toast({ title: '已从自选股移除', status: 'info', duration: 1500 });
} else {
toast({ title: '移除失败', status: 'error', duration: 2000 });
}
const stockName = stockItem?.stock_name || '';
// 通过 Redux action 移除(会同步更新 Redux 状态)
await dispatch(toggleWatchlistAction({
stockCode,
stockName,
isInWatchlist: true // 表示当前在自选股中,需要移除
})).unwrap();
// 更新本地状态(立即响应 UI
setWatchlistQuotes((prev) => {
const normalize6 = (code) => {
const m = String(code || '').match(/(\d{6})/);
return m ? m[1] : String(code || '');
};
const target = normalize6(stockCode);
const updated = (prev || []).filter((x) => normalize6(x.stock_code) !== target);
const newMaxPage = Math.max(1, Math.ceil((updated.length || 0) / WATCHLIST_PAGE_SIZE));
setWatchlistPage((p) => Math.min(p, newMaxPage));
return updated;
});
toast({ title: '已从自选股移除', status: 'info', duration: 1500 });
} catch (e) {
toast({ title: '网络错误,移除失败', status: 'error', duration: 2000 });
return false;
logger.error('useWatchlist', '移除自选股失败', e);
toast({ title: e.message || '移除失败', status: 'error', duration: 2000 });
}
}, [toast]);
}, [dispatch, watchlistQuotes, toast]);
// 判断股票是否在自选股中
const isInWatchlist = useCallback((stockCode) => {
@@ -134,6 +197,7 @@ export const useWatchlist = () => {
setWatchlistPage,
WATCHLIST_PAGE_SIZE,
loadWatchlistQuotes,
followingEvents,
handleAddToWatchlist,
handleRemoveFromWatchlist,
isInWatchlist

View File

@@ -7,6 +7,18 @@ import MiniTimelineChart from './MiniTimelineChart';
import { fetchBatchKlineData, klineDataCache, getCacheKey } from '../utils/klineDataCache';
import { logger } from '../../../../../utils/logger';
/**
* 标准化股票代码为6位格式
* @param {string} code - 股票代码
* @returns {string} 6位标准化代码
*/
const normalizeStockCode = (code) => {
if (!code) return '';
const s = String(code).trim();
const m = s.match(/(\d{6})/);
return m ? m[1] : s;
};
/**
* 股票列表表格组件
* 显示事件相关股票列表,包括分时图、涨跌幅、自选股操作等
@@ -260,7 +272,9 @@ const StockTable = ({
width: 150,
fixed: 'right',
render: (_, record) => {
const isInWatchlist = watchlistSet.has(record.stock_code);
// 标准化代码后再比较,确保 600000.SH 和 600000 能匹配
const normalizedCode = normalizeStockCode(record.stock_code);
const isInWatchlist = watchlistSet.has(normalizedCode);
return (
<div style={{ display: 'flex', gap: '4px' }}>
<Button

View File

@@ -5,6 +5,27 @@ import { loadWatchlist, toggleWatchlist as toggleWatchlistAction } from '../../.
import { message } from 'antd';
import { logger } from '../../../../../utils/logger';
/**
* 标准化股票代码为6位格式
* 支持: 600000, 600000.SH, 600000.SZ, SH600000 等格式
* @param {string} code - 股票代码
* @returns {string} 6位标准化代码
*/
const normalizeStockCode = (code) => {
if (!code) return '';
const s = String(code).trim().toUpperCase();
// 匹配6位数字可能带 .SH/.SZ 后缀)
const m1 = s.match(/^(\d{6})(?:\.(?:SH|SZ))?$/i);
if (m1) return m1[1];
// 匹配 SH/SZ 前缀格式
const m2 = s.match(/^(?:SH|SZ)(\d{6})$/i);
if (m2) return m2[1];
// 尝试提取任意6位数字
const m3 = s.match(/(\d{6})/);
if (m3) return m3[1];
return s;
};
/**
* 自选股管理 Hook
* 封装自选股的加载、添加、移除逻辑
@@ -19,9 +40,9 @@ export const useWatchlist = (shouldLoad = true) => {
const watchlistArray = useSelector(state => state.stock.watchlist, shallowEqual);
const loading = useSelector(state => state.stock.loading.watchlist);
// 转换为 Set 方便快速查询
// 转换为 Set 方便快速查询标准化为6位代码
const watchlistSet = useMemo(() => {
return new Set(watchlistArray);
return new Set(watchlistArray.map(normalizeStockCode));
}, [watchlistArray]);
// 初始化时加载自选股列表(只在 shouldLoad 为 true 时)
@@ -33,32 +54,36 @@ export const useWatchlist = (shouldLoad = true) => {
}, [dispatch, shouldLoad]);
/**
* 检查股票是否在自选股中
* @param {string} stockCode - 股票代码
* 检查股票是否在自选股中(支持带后缀的代码格式)
* @param {string} stockCode - 股票代码(支持 600000, 600000.SH 等格式)
* @returns {boolean}
*/
const isInWatchlist = useCallback((stockCode) => {
return watchlistSet.has(stockCode);
const normalized = normalizeStockCode(stockCode);
return watchlistSet.has(normalized);
}, [watchlistSet]);
/**
* 切换自选股状态
* @param {string} stockCode - 股票代码
* @param {string} stockCode - 股票代码(支持带后缀格式,会自动标准化)
* @param {string} stockName - 股票名称
* @returns {Promise<boolean>} 操作是否成功
*/
const toggleWatchlist = useCallback(async (stockCode, stockName) => {
const wasInWatchlist = watchlistSet.has(stockCode);
const normalized = normalizeStockCode(stockCode);
const wasInWatchlist = watchlistSet.has(normalized);
logger.debug('useWatchlist', '切换自选股状态', {
stockCode,
normalized,
stockName,
wasInWatchlist
});
try {
// 传递标准化后的6位代码给 Redux action
await dispatch(toggleWatchlistAction({
stockCode,
stockCode: normalized,
stockName,
isInWatchlist: wasInWatchlist
})).unwrap();
@@ -68,6 +93,7 @@ export const useWatchlist = (shouldLoad = true) => {
} catch (error) {
logger.error('useWatchlist', '切换自选股失败', error, {
stockCode,
normalized,
stockName
});
message.error(error.message || '操作失败,请稍后重试');
@@ -87,16 +113,17 @@ export const useWatchlist = (shouldLoad = true) => {
let successCount = 0;
const promises = stocks.map(async ({ code, name }) => {
if (!watchlistSet.has(code)) {
const normalized = normalizeStockCode(code);
if (!watchlistSet.has(normalized)) {
try {
await dispatch(toggleWatchlistAction({
stockCode: code,
stockCode: normalized,
stockName: name,
isInWatchlist: false
})).unwrap();
successCount++;
} catch (error) {
logger.error('useWatchlist', '添加失败', error, { code, name });
logger.error('useWatchlist', '添加失败', error, { code, normalized, name });
}
}
});