diff --git a/MeAgent/ios/app.xcodeproj/project.pbxproj b/MeAgent/ios/app.xcodeproj/project.pbxproj
index 3322cf0e..59461e5a 100644
--- a/MeAgent/ios/app.xcodeproj/project.pbxproj
+++ b/MeAgent/ios/app.xcodeproj/project.pbxproj
@@ -460,7 +460,10 @@
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
- OTHER_LDFLAGS = "$(inherited) ";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ " ",
+ );
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
@@ -518,7 +521,10 @@
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
- OTHER_LDFLAGS = "$(inherited) ";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ " ",
+ );
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
diff --git a/MeAgent/navigation/Screens.js b/MeAgent/navigation/Screens.js
index 31d0f6d5..a3be405f 100644
--- a/MeAgent/navigation/Screens.js
+++ b/MeAgent/navigation/Screens.js
@@ -323,6 +323,8 @@ function MarketStack(props) {
diff --git a/MeAgent/src/screens/Agent/components/MarkdownRenderer.js b/MeAgent/src/screens/Agent/components/MarkdownRenderer.js
index 1a2a3d63..53de6e8a 100644
--- a/MeAgent/src/screens/Agent/components/MarkdownRenderer.js
+++ b/MeAgent/src/screens/Agent/components/MarkdownRenderer.js
@@ -3,7 +3,7 @@
* 支持 Markdown 渲染和 ECharts 图表渲染
*/
-import React, { memo, useMemo, useState, useCallback } from 'react';
+import React, { memo, useMemo, useState, useCallback, useContext, createContext } from 'react';
import {
View,
Text,
@@ -14,8 +14,12 @@ import {
ActivityIndicator,
} from 'react-native';
import { WebView } from 'react-native-webview';
+import { useNavigation } from '@react-navigation/native';
import { AgentTheme } from '../../../constants/agentConstants';
+// 股票代码正则:匹配 000001.SZ, 600000.SH, 或纯六位数字
+const STOCK_CODE_REGEX = /\b(\d{6})(?:\.(SZ|SH|sz|sh|BJ|bj))?\b/g;
+
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const CHART_WIDTH = SCREEN_WIDTH - 48; // 减去边距
const CHART_HEIGHT = 300;
@@ -137,7 +141,7 @@ const TableView = memo(({ rows }) => {
/**
* Markdown 文本渲染器(支持表格)
*/
-const MarkdownText = memo(({ content }) => {
+const MarkdownText = memo(({ content, navigation }) => {
const lines = content.split('\n');
const elements = [];
let i = 0;
@@ -155,7 +159,7 @@ const MarkdownText = memo(({ content }) => {
}
}
- elements.push();
+ elements.push();
i++;
}
@@ -169,7 +173,7 @@ const MarkdownText = memo(({ content }) => {
/**
* 单行 Markdown 渲染
*/
-const MarkdownLine = memo(({ line }) => {
+const MarkdownLine = memo(({ line, navigation }) => {
// 空行
if (!line.trim()) {
return ;
@@ -188,7 +192,7 @@ const MarkdownLine = memo(({ line }) => {
];
return (
- {renderInlineStyles(text)}
+ {renderInlineStyles(text, navigation)}
);
}
@@ -199,7 +203,7 @@ const MarkdownLine = memo(({ line }) => {
return (
•
- {renderInlineStyles(listMatch[1])}
+ {renderInlineStyles(listMatch[1], navigation)}
);
}
@@ -210,7 +214,7 @@ const MarkdownLine = memo(({ line }) => {
return (
{orderedListMatch[1]}.
- {renderInlineStyles(orderedListMatch[2])}
+ {renderInlineStyles(orderedListMatch[2], navigation)}
);
}
@@ -225,7 +229,7 @@ const MarkdownLine = memo(({ line }) => {
if (quoteMatch) {
return (
- {renderInlineStyles(quoteMatch[1])}
+ {renderInlineStyles(quoteMatch[1], navigation)}
);
}
@@ -237,32 +241,113 @@ const MarkdownLine = memo(({ line }) => {
// 普通段落
return (
- {renderInlineStyles(line)}
+ {renderInlineStyles(line, navigation)}
);
});
/**
- * 渲染行内样式(粗体、斜体、代码、链接)
+ * 股票代码可点击组件
*/
-const renderInlineStyles = (text) => {
+const StockCodeLink = memo(({ code, exchange, navigation }) => {
+ // 确定交易所后缀
+ let exchangeSuffix = exchange?.toUpperCase() || '';
+ if (!exchangeSuffix) {
+ // 根据代码前缀推断交易所
+ if (code.startsWith('6')) {
+ exchangeSuffix = 'SH';
+ } else if (code.startsWith('0') || code.startsWith('3')) {
+ exchangeSuffix = 'SZ';
+ } else if (code.startsWith('4') || code.startsWith('8')) {
+ exchangeSuffix = 'BJ';
+ }
+ }
+
+ const handlePress = useCallback(() => {
+ const stockCode = exchangeSuffix ? `${code}.${exchangeSuffix}` : code;
+ navigation?.navigate('StockDetail', {
+ code: stockCode,
+ name: stockCode, // 名称可以在详情页获取
+ });
+ }, [code, exchangeSuffix, navigation]);
+
+ const displayCode = exchangeSuffix ? `${code}.${exchangeSuffix}` : code;
+
+ return (
+
+ {displayCode}
+
+ );
+});
+
+/**
+ * 渲染行内样式(粗体、斜体、代码、链接、股票代码)
+ */
+const renderInlineStyles = (text, navigation) => {
if (!text) return null;
+ // 先进行基本文本处理
+ const processedText = text
+ .replace(/\*\*(.+?)\*\*/g, (_, m) => m) // 移除粗体标记(简化)
+ .replace(/\*(.+?)\*/g, (_, m) => m) // 移除斜体标记
+ .replace(/`([^`]+)`/g, (_, m) => `[${m}]`); // 标记代码
+
+ // 检测股票代码并分割文本
+ const stockMatches = [];
+ let match;
+ const regex = new RegExp(STOCK_CODE_REGEX.source, 'g');
+ while ((match = regex.exec(processedText)) !== null) {
+ stockMatches.push({
+ code: match[1],
+ exchange: match[2],
+ index: match.index,
+ length: match[0].length,
+ fullMatch: match[0],
+ });
+ }
+
+ // 如果没有股票代码,返回普通文本
+ if (stockMatches.length === 0) {
+ return {processedText};
+ }
+
+ // 有股票代码,需要分割并渲染
const elements = [];
+ let lastIndex = 0;
let key = 0;
- // 为简化实现,这里只做基本渲染
- // 完整实现需要递归解析
- elements.push(
-
- {text
- .replace(/\*\*(.+?)\*\*/g, (_, m) => m) // 移除粗体标记(简化)
- .replace(/\*(.+?)\*/g, (_, m) => m) // 移除斜体标记
- .replace(/`([^`]+)`/g, (_, m) => `[${m}]`) // 标记代码
- }
-
- );
+ stockMatches.forEach((stockMatch) => {
+ // 添加股票代码前的文本
+ if (stockMatch.index > lastIndex) {
+ elements.push(
+
+ {processedText.substring(lastIndex, stockMatch.index)}
+
+ );
+ }
- return elements;
+ // 添加可点击的股票代码
+ elements.push(
+
+ );
+
+ lastIndex = stockMatch.index + stockMatch.length;
+ });
+
+ // 添加最后剩余的文本
+ if (lastIndex < processedText.length) {
+ elements.push(
+
+ {processedText.substring(lastIndex)}
+
+ );
+ }
+
+ return {elements};
};
/**
@@ -670,6 +755,7 @@ const MermaidView = memo(({ code }) => {
* MarkdownRenderer 主组件
*/
const MarkdownRenderer = ({ content }) => {
+ const navigation = useNavigation();
const parts = useMemo(() => parseMarkdown(content), [content]);
return (
@@ -681,7 +767,7 @@ const MarkdownRenderer = ({ content }) => {
if (part.type === 'mermaid') {
return ;
}
- return ;
+ return ;
})}
);
@@ -795,6 +881,16 @@ const styles = StyleSheet.create({
marginVertical: 16,
},
+ // 股票代码链接
+ stockCodeLink: {
+ color: '#F59E0B',
+ fontWeight: '600',
+ textDecorationLine: 'underline',
+ backgroundColor: 'rgba(245, 158, 11, 0.1)',
+ paddingHorizontal: 4,
+ borderRadius: 4,
+ },
+
// 图表容器
chartContainer: {
backgroundColor: 'rgba(99, 102, 241, 0.1)',
diff --git a/MeAgent/src/screens/Agent/components/WelcomeScreen.js b/MeAgent/src/screens/Agent/components/WelcomeScreen.js
index c2f13105..b045e5f8 100644
--- a/MeAgent/src/screens/Agent/components/WelcomeScreen.js
+++ b/MeAgent/src/screens/Agent/components/WelcomeScreen.js
@@ -95,38 +95,6 @@ const WelcomeScreen = ({ onQuickQuestion }) => {
在市场的混沌中,找到价值与风险的平衡点
- {/* Bento Grid 功能展示 */}
-
-
-
-
-
-
-
-
-
-
-
{/* 快捷问题 */}
diff --git a/MeAgent/src/screens/Concepts/ConceptList.js b/MeAgent/src/screens/Concepts/ConceptList.js
index 1efa692e..8fc22d12 100644
--- a/MeAgent/src/screens/Concepts/ConceptList.js
+++ b/MeAgent/src/screens/Concepts/ConceptList.js
@@ -630,7 +630,7 @@ const ConceptList = () => {
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState(null);
const [drillPath, setDrillPath] = useState([]);
- const [viewMode, setViewMode] = useState('card'); // 'card' | 'list'
+ const [viewMode, setViewMode] = useState('list'); // 'card' | 'list'
const [sortBy, setSortBy] = useState('change'); // 'change' | 'outbreak'
const [leafConcepts, setLeafConcepts] = useState([]); // 列表模式下的叶子概念
const [leafLoading, setLeafLoading] = useState(false);
@@ -1154,7 +1154,7 @@ const ConceptList = () => {
) : (
// 父分类卡片列表
-
+
{currentItems.map((item, index) => (
{
)
)}
+
+ handleModeChange(VIEW_MODES.STATS)}>
+ {({ isPressed }) => (
+ viewMode === VIEW_MODES.STATS ? (
+
+
+
+ 统计
+
+
+ ) : (
+
+
+
+ 统计
+
+
+ )
+ )}
+
);
@@ -479,6 +509,19 @@ const EventList = ({ navigation }) => {
const keyExtractor = useCallback((item) => `event-${item.id}`, []);
+ // 统计视图模式
+ if (viewMode === VIEW_MODES.STATS) {
+ return (
+
+
+
+ {renderHeader()}
+
+
+
+ );
+ }
+
// 题材视图模式
if (viewMode === VIEW_MODES.MAINLINE) {
return (
diff --git a/MeAgent/src/screens/Market/MarketHot.js b/MeAgent/src/screens/Market/MarketHot.js
index d05acac4..bf11d3a2 100644
--- a/MeAgent/src/screens/Market/MarketHot.js
+++ b/MeAgent/src/screens/Market/MarketHot.js
@@ -27,10 +27,20 @@ import { Ionicons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import ztService from '../../services/ztService';
+import tradingDayUtils from '../../utils/tradingDayUtils';
import { gradients } from '../../theme';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
+// 获取初始日期(如果今天不是交易日,返回上一个交易日)
+const getInitialDate = () => {
+ const today = new Date();
+ if (tradingDayUtils.isTradingDay(today)) {
+ return today;
+ }
+ return tradingDayUtils.getPreviousTradingDay(today);
+};
+
// 日期格式化
const formatDisplayDate = (date) => {
const d = new Date(date);
@@ -73,7 +83,7 @@ const parseContinuousDays = (str) => {
const MarketHot = ({ navigation }) => {
const insets = useSafeAreaInsets();
- const [currentDate, setCurrentDate] = useState(new Date());
+ const [currentDate, setCurrentDate] = useState(getInitialDate);
const [ztData, setZtData] = useState(null);
const [stats, setStats] = useState(null);
const [hotSectors, setHotSectors] = useState([]);
@@ -179,24 +189,14 @@ const MarketHot = ({ navigation }) => {
-
- navigation.navigate('TodayStats')}
- p={2}
- rounded="full"
- bg="rgba(255,255,255,0.1)"
- >
-
-
- navigation.navigate('EventCalendar')}
- p={2}
- rounded="full"
- bg="rgba(255,255,255,0.1)"
- >
-
-
-
+ navigation.navigate('EventCalendar')}
+ p={2}
+ rounded="full"
+ bg="rgba(255,255,255,0.1)"
+ >
+
+
@@ -212,7 +212,7 @@ const MarketHot = ({ navigation }) => {
- setCurrentDate(new Date())}>
+ setCurrentDate(getInitialDate())}>
{formatDisplayDate(currentDate)}
diff --git a/MeAgent/src/screens/Market/TodayStats.js b/MeAgent/src/screens/Market/TodayStats.js
index 955ffcfc..15a7b169 100644
--- a/MeAgent/src/screens/Market/TodayStats.js
+++ b/MeAgent/src/screens/Market/TodayStats.js
@@ -302,7 +302,7 @@ const TopStockItem = ({ stock, rank }) => (
);
-const TodayStats = ({ navigation }) => {
+const TodayStats = ({ navigation, isEmbedded = false }) => {
const insets = useSafeAreaInsets();
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
@@ -364,7 +364,7 @@ const TodayStats = ({ navigation }) => {
{
/>
}
>
- {/* 标题栏 */}
-
-
-
- navigation.goBack()}
- p={2}
- mr={2}
- rounded="full"
- bg="rgba(255,255,255,0.05)"
- >
-
-
-
-
-
- 今日统计
+ {/* 标题栏 - 仅在非嵌入模式下显示 */}
+ {!isEmbedded && (
+
+
+
+ navigation.goBack()}
+ p={2}
+ mr={2}
+ rounded="full"
+ bg="rgba(255,255,255,0.05)"
+ >
+
+
+
+
+
+ 今日统计
+
+
+
+
+ 实时
+
+
+
+
+ 事件胜率 · 市场统计 · TOP排行
-
-
-
- 实时
-
-
-
-
- 事件胜率 · 市场统计 · TOP排行
-
-
+
+
+ navigation.openDrawer?.()}
+ p={2}
+ rounded="full"
+ bg="rgba(255,255,255,0.1)"
+ >
+
+
- navigation.openDrawer?.()}
- p={2}
- rounded="full"
- bg="rgba(255,255,255,0.1)"
- >
-
-
-
-
+
+ )}
{/* 双圆环仪表盘 */}
diff --git a/MeAgent/src/services/api.js b/MeAgent/src/services/api.js
index aa1ffc31..a5f280c9 100644
--- a/MeAgent/src/services/api.js
+++ b/MeAgent/src/services/api.js
@@ -3,11 +3,16 @@
* 复用 Web 端的 API 逻辑,适配 React Native
*/
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
export const API_BASE_URL = 'https://api.valuefrontier.cn';
// 静态数据存储地址(腾讯 COS)
export const API_BASE = 'https://valuefrontier-1308417363.cos-website.ap-shanghai.myqcloud.com';
+// Token 存储键名(与 authService 保持一致)
+const ACCESS_TOKEN_KEY = '@auth_access_token';
+
/**
* 通用 API 请求函数
* @param {string} url - API 路径
@@ -21,13 +26,24 @@ export const apiRequest = async (url, options = {}) => {
console.log(`[API] ${method} ${fullUrl}`);
try {
+ // 获取存储的 access_token
+ const accessToken = await AsyncStorage.getItem(ACCESS_TOKEN_KEY);
+
+ // 构建请求头
+ const headers = {
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ };
+
+ // 如果有 token,添加到 Authorization header
+ if (accessToken) {
+ headers['Authorization'] = `Bearer ${accessToken}`;
+ }
+
const response = await fetch(fullUrl, {
...options,
- headers: {
- 'Content-Type': 'application/json',
- ...options.headers,
- },
- // 重要:携带 Cookie 以支持 Session 认证
+ headers,
+ // 保留 credentials 以支持 Cookie(作为备选认证方式)
credentials: 'include',
});
diff --git a/MeAgent/src/services/authService.js b/MeAgent/src/services/authService.js
index 8f2a1cd5..c92b49b6 100644
--- a/MeAgent/src/services/authService.js
+++ b/MeAgent/src/services/authService.js
@@ -10,6 +10,7 @@ import { apiRequest } from './api';
const STORAGE_KEYS = {
USER_INFO: '@auth_user_info',
IS_LOGGED_IN: '@auth_is_logged_in',
+ ACCESS_TOKEN: '@auth_access_token',
};
/**
@@ -64,6 +65,13 @@ export const authService = {
// 保存用户信息到本地存储
await AsyncStorage.setItem(STORAGE_KEYS.USER_INFO, JSON.stringify(response.user));
await AsyncStorage.setItem(STORAGE_KEYS.IS_LOGGED_IN, 'true');
+
+ // 保存 access_token(如果后端返回)
+ if (response.access_token) {
+ await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, response.access_token);
+ console.log('[AuthService] 已保存 access_token');
+ }
+
console.log('[AuthService] 登录成功,用户:', response.user.username);
}
@@ -125,11 +133,25 @@ export const authService = {
try {
await AsyncStorage.removeItem(STORAGE_KEYS.USER_INFO);
await AsyncStorage.removeItem(STORAGE_KEYS.IS_LOGGED_IN);
+ await AsyncStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
} catch (error) {
console.error('[AuthService] 清除本地状态失败:', error);
}
},
+ /**
+ * 获取存储的 access_token
+ * @returns {Promise}
+ */
+ getAccessToken: async () => {
+ try {
+ return await AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
+ } catch (error) {
+ console.error('[AuthService] 读取 token 失败:', error);
+ return null;
+ }
+ },
+
/**
* 获取本地存储的用户信息
* @returns {Promise