更新ios
This commit is contained in:
@@ -460,7 +460,10 @@
|
|||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
OTHER_LDFLAGS = "$(inherited) ";
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
" ",
|
||||||
|
);
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
USE_HERMES = true;
|
USE_HERMES = true;
|
||||||
@@ -518,7 +521,10 @@
|
|||||||
);
|
);
|
||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
OTHER_LDFLAGS = "$(inherited) ";
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
" ",
|
||||||
|
);
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
USE_HERMES = true;
|
USE_HERMES = true;
|
||||||
|
|||||||
@@ -323,6 +323,8 @@ function MarketStack(props) {
|
|||||||
<Header
|
<Header
|
||||||
title={route.params?.sectorName || "板块详情"}
|
title={route.params?.sectorName || "板块详情"}
|
||||||
back
|
back
|
||||||
|
white
|
||||||
|
bgColor="#0F172A"
|
||||||
navigation={navigation}
|
navigation={navigation}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 支持 Markdown 渲染和 ECharts 图表渲染
|
* 支持 Markdown 渲染和 ECharts 图表渲染
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { memo, useMemo, useState, useCallback } from 'react';
|
import React, { memo, useMemo, useState, useCallback, useContext, createContext } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -14,8 +14,12 @@ import {
|
|||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { WebView } from 'react-native-webview';
|
import { WebView } from 'react-native-webview';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { AgentTheme } from '../../../constants/agentConstants';
|
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 { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||||
const CHART_WIDTH = SCREEN_WIDTH - 48; // 减去边距
|
const CHART_WIDTH = SCREEN_WIDTH - 48; // 减去边距
|
||||||
const CHART_HEIGHT = 300;
|
const CHART_HEIGHT = 300;
|
||||||
@@ -137,7 +141,7 @@ const TableView = memo(({ rows }) => {
|
|||||||
/**
|
/**
|
||||||
* Markdown 文本渲染器(支持表格)
|
* Markdown 文本渲染器(支持表格)
|
||||||
*/
|
*/
|
||||||
const MarkdownText = memo(({ content }) => {
|
const MarkdownText = memo(({ content, navigation }) => {
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
const elements = [];
|
const elements = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -155,7 +159,7 @@ const MarkdownText = memo(({ content }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.push(<MarkdownLine key={i} line={line} />);
|
elements.push(<MarkdownLine key={i} line={line} navigation={navigation} />);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +173,7 @@ const MarkdownText = memo(({ content }) => {
|
|||||||
/**
|
/**
|
||||||
* 单行 Markdown 渲染
|
* 单行 Markdown 渲染
|
||||||
*/
|
*/
|
||||||
const MarkdownLine = memo(({ line }) => {
|
const MarkdownLine = memo(({ line, navigation }) => {
|
||||||
// 空行
|
// 空行
|
||||||
if (!line.trim()) {
|
if (!line.trim()) {
|
||||||
return <View style={styles.emptyLine} />;
|
return <View style={styles.emptyLine} />;
|
||||||
@@ -188,7 +192,7 @@ const MarkdownLine = memo(({ line }) => {
|
|||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<Text style={[styles.headerBase, headerStyles[level - 1]]}>
|
<Text style={[styles.headerBase, headerStyles[level - 1]]}>
|
||||||
{renderInlineStyles(text)}
|
{renderInlineStyles(text, navigation)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -199,7 +203,7 @@ const MarkdownLine = memo(({ line }) => {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.listItem}>
|
<View style={styles.listItem}>
|
||||||
<Text style={styles.listBullet}>•</Text>
|
<Text style={styles.listBullet}>•</Text>
|
||||||
<Text style={styles.listText}>{renderInlineStyles(listMatch[1])}</Text>
|
<Text style={styles.listText}>{renderInlineStyles(listMatch[1], navigation)}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -210,7 +214,7 @@ const MarkdownLine = memo(({ line }) => {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.listItem}>
|
<View style={styles.listItem}>
|
||||||
<Text style={styles.listNumber}>{orderedListMatch[1]}.</Text>
|
<Text style={styles.listNumber}>{orderedListMatch[1]}.</Text>
|
||||||
<Text style={styles.listText}>{renderInlineStyles(orderedListMatch[2])}</Text>
|
<Text style={styles.listText}>{renderInlineStyles(orderedListMatch[2], navigation)}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -225,7 +229,7 @@ const MarkdownLine = memo(({ line }) => {
|
|||||||
if (quoteMatch) {
|
if (quoteMatch) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.quote}>
|
<View style={styles.quote}>
|
||||||
<Text style={styles.quoteText}>{renderInlineStyles(quoteMatch[1])}</Text>
|
<Text style={styles.quoteText}>{renderInlineStyles(quoteMatch[1], navigation)}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -237,32 +241,113 @@ const MarkdownLine = memo(({ line }) => {
|
|||||||
|
|
||||||
// 普通段落
|
// 普通段落
|
||||||
return (
|
return (
|
||||||
<Text style={styles.paragraph}>{renderInlineStyles(line)}</Text>
|
<Text style={styles.paragraph}>{renderInlineStyles(line, navigation)}</Text>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染行内样式(粗体、斜体、代码、链接)
|
* 股票代码可点击组件
|
||||||
*/
|
*/
|
||||||
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 (
|
||||||
|
<TouchableOpacity onPress={handlePress} activeOpacity={0.7}>
|
||||||
|
<Text style={styles.stockCodeLink}>{displayCode}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染行内样式(粗体、斜体、代码、链接、股票代码)
|
||||||
|
*/
|
||||||
|
const renderInlineStyles = (text, navigation) => {
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
|
|
||||||
const elements = [];
|
// 先进行基本文本处理
|
||||||
let key = 0;
|
const processedText = text
|
||||||
|
|
||||||
// 为简化实现,这里只做基本渲染
|
|
||||||
// 完整实现需要递归解析
|
|
||||||
elements.push(
|
|
||||||
<Text key={key++} style={styles.text}>
|
|
||||||
{text
|
|
||||||
.replace(/\*\*(.+?)\*\*/g, (_, m) => m) // 移除粗体标记(简化)
|
.replace(/\*\*(.+?)\*\*/g, (_, m) => m) // 移除粗体标记(简化)
|
||||||
.replace(/\*(.+?)\*/g, (_, m) => m) // 移除斜体标记
|
.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 <Text style={styles.text}>{processedText}</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有股票代码,需要分割并渲染
|
||||||
|
const elements = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
let key = 0;
|
||||||
|
|
||||||
|
stockMatches.forEach((stockMatch) => {
|
||||||
|
// 添加股票代码前的文本
|
||||||
|
if (stockMatch.index > lastIndex) {
|
||||||
|
elements.push(
|
||||||
|
<Text key={key++} style={styles.text}>
|
||||||
|
{processedText.substring(lastIndex, stockMatch.index)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return elements;
|
// 添加可点击的股票代码
|
||||||
|
elements.push(
|
||||||
|
<StockCodeLink
|
||||||
|
key={key++}
|
||||||
|
code={stockMatch.code}
|
||||||
|
exchange={stockMatch.exchange}
|
||||||
|
navigation={navigation}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
lastIndex = stockMatch.index + stockMatch.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加最后剩余的文本
|
||||||
|
if (lastIndex < processedText.length) {
|
||||||
|
elements.push(
|
||||||
|
<Text key={key++} style={styles.text}>
|
||||||
|
{processedText.substring(lastIndex)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Text>{elements}</Text>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -670,6 +755,7 @@ const MermaidView = memo(({ code }) => {
|
|||||||
* MarkdownRenderer 主组件
|
* MarkdownRenderer 主组件
|
||||||
*/
|
*/
|
||||||
const MarkdownRenderer = ({ content }) => {
|
const MarkdownRenderer = ({ content }) => {
|
||||||
|
const navigation = useNavigation();
|
||||||
const parts = useMemo(() => parseMarkdown(content), [content]);
|
const parts = useMemo(() => parseMarkdown(content), [content]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -681,7 +767,7 @@ const MarkdownRenderer = ({ content }) => {
|
|||||||
if (part.type === 'mermaid') {
|
if (part.type === 'mermaid') {
|
||||||
return <MermaidView key={index} code={part.content} />;
|
return <MermaidView key={index} code={part.content} />;
|
||||||
}
|
}
|
||||||
return <MarkdownText key={index} content={part.content} />;
|
return <MarkdownText key={index} content={part.content} navigation={navigation} />;
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -795,6 +881,16 @@ const styles = StyleSheet.create({
|
|||||||
marginVertical: 16,
|
marginVertical: 16,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 股票代码链接
|
||||||
|
stockCodeLink: {
|
||||||
|
color: '#F59E0B',
|
||||||
|
fontWeight: '600',
|
||||||
|
textDecorationLine: 'underline',
|
||||||
|
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
|
||||||
// 图表容器
|
// 图表容器
|
||||||
chartContainer: {
|
chartContainer: {
|
||||||
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
||||||
|
|||||||
@@ -95,38 +95,6 @@ const WelcomeScreen = ({ onQuickQuestion }) => {
|
|||||||
<Text style={styles.tagline}>在市场的混沌中,找到价值与风险的平衡点</Text>
|
<Text style={styles.tagline}>在市场的混沌中,找到价值与风险的平衡点</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Bento Grid 功能展示 */}
|
|
||||||
<View style={styles.bentoGrid}>
|
|
||||||
<View style={styles.bentoRow}>
|
|
||||||
<BentoCard
|
|
||||||
icon="📊"
|
|
||||||
title="深度分析"
|
|
||||||
description="基本面与技术面"
|
|
||||||
color="#8B5CF6"
|
|
||||||
/>
|
|
||||||
<BentoCard
|
|
||||||
icon="🔥"
|
|
||||||
title="热点追踪"
|
|
||||||
description="涨停板块监控"
|
|
||||||
color="#EC4899"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={styles.bentoRow}>
|
|
||||||
<BentoCard
|
|
||||||
icon="📈"
|
|
||||||
title="趋势研究"
|
|
||||||
description="行业投资机会"
|
|
||||||
color="#10B981"
|
|
||||||
/>
|
|
||||||
<BentoCard
|
|
||||||
icon="🧮"
|
|
||||||
title="量化分析"
|
|
||||||
description="因子指标计算"
|
|
||||||
color="#F59E0B"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 快捷问题 */}
|
{/* 快捷问题 */}
|
||||||
<View style={styles.quickSection}>
|
<View style={styles.quickSection}>
|
||||||
<View style={styles.sectionHeader}>
|
<View style={styles.sectionHeader}>
|
||||||
|
|||||||
@@ -630,7 +630,7 @@ const ConceptList = () => {
|
|||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [drillPath, setDrillPath] = useState([]);
|
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 [sortBy, setSortBy] = useState('change'); // 'change' | 'outbreak'
|
||||||
const [leafConcepts, setLeafConcepts] = useState([]); // 列表模式下的叶子概念
|
const [leafConcepts, setLeafConcepts] = useState([]); // 列表模式下的叶子概念
|
||||||
const [leafLoading, setLeafLoading] = useState(false);
|
const [leafLoading, setLeafLoading] = useState(false);
|
||||||
@@ -1154,7 +1154,7 @@ const ConceptList = () => {
|
|||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
// 父分类卡片列表
|
// 父分类卡片列表
|
||||||
<VStack space={4}>
|
<VStack space={4} w="100%">
|
||||||
{currentItems.map((item, index) => (
|
{currentItems.map((item, index) => (
|
||||||
<ParentCard
|
<ParentCard
|
||||||
key={item.name || index}
|
key={item.name || index}
|
||||||
@@ -1197,6 +1197,7 @@ const styles = StyleSheet.create({
|
|||||||
parentCard: {
|
parentCard: {
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
width: '100%',
|
||||||
},
|
},
|
||||||
cardContainer: {
|
cardContainer: {
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||||||
|
|
||||||
import EventCard from './EventCard';
|
import EventCard from './EventCard';
|
||||||
import MainlineView from './MainlineView';
|
import MainlineView from './MainlineView';
|
||||||
|
import TodayStats from '../Market/TodayStats';
|
||||||
import { gradients } from '../../theme';
|
import { gradients } from '../../theme';
|
||||||
import {
|
import {
|
||||||
fetchEvents,
|
fetchEvents,
|
||||||
@@ -36,6 +37,7 @@ const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
|||||||
const VIEW_MODES = {
|
const VIEW_MODES = {
|
||||||
LIST: 'list',
|
LIST: 'list',
|
||||||
MAINLINE: 'mainline',
|
MAINLINE: 'mainline',
|
||||||
|
STATS: 'stats',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 时间范围选项
|
// 时间范围选项
|
||||||
@@ -205,6 +207,34 @@ const EventList = ({ navigation }) => {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
|
<Pressable onPress={() => handleModeChange(VIEW_MODES.STATS)}>
|
||||||
|
{({ isPressed }) => (
|
||||||
|
viewMode === VIEW_MODES.STATS ? (
|
||||||
|
<LinearGradient
|
||||||
|
colors={['#F59E0B', '#D97706']}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 0 }}
|
||||||
|
style={styles.modeButtonActive}
|
||||||
|
>
|
||||||
|
<Icon as={Ionicons} name="stats-chart" size="sm" color="white" />
|
||||||
|
<Text fontSize="xs" fontWeight="bold" color="white" ml={1.5}>
|
||||||
|
统计
|
||||||
|
</Text>
|
||||||
|
</LinearGradient>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
style={styles.modeButton}
|
||||||
|
opacity={isPressed ? 0.7 : 1}
|
||||||
|
>
|
||||||
|
<Icon as={Ionicons} name="stats-chart" size="sm" color="gray.400" />
|
||||||
|
<Text fontSize="xs" color="gray.400" ml={1.5}>
|
||||||
|
统计
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -479,6 +509,19 @@ const EventList = ({ navigation }) => {
|
|||||||
|
|
||||||
const keyExtractor = useCallback((item) => `event-${item.id}`, []);
|
const keyExtractor = useCallback((item) => `event-${item.id}`, []);
|
||||||
|
|
||||||
|
// 统计视图模式
|
||||||
|
if (viewMode === VIEW_MODES.STATS) {
|
||||||
|
return (
|
||||||
|
<Box flex={1} bg="#0F172A">
|
||||||
|
<StatusBar barStyle="light-content" />
|
||||||
|
<VStack style={{ paddingTop: insets.top }}>
|
||||||
|
{renderHeader()}
|
||||||
|
</VStack>
|
||||||
|
<TodayStats navigation={navigation} isEmbedded />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 题材视图模式
|
// 题材视图模式
|
||||||
if (viewMode === VIEW_MODES.MAINLINE) {
|
if (viewMode === VIEW_MODES.MAINLINE) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -27,10 +27,20 @@ import { Ionicons } from '@expo/vector-icons';
|
|||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import ztService from '../../services/ztService';
|
import ztService from '../../services/ztService';
|
||||||
|
import tradingDayUtils from '../../utils/tradingDayUtils';
|
||||||
import { gradients } from '../../theme';
|
import { gradients } from '../../theme';
|
||||||
|
|
||||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
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 formatDisplayDate = (date) => {
|
||||||
const d = new Date(date);
|
const d = new Date(date);
|
||||||
@@ -73,7 +83,7 @@ const parseContinuousDays = (str) => {
|
|||||||
|
|
||||||
const MarketHot = ({ navigation }) => {
|
const MarketHot = ({ navigation }) => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(getInitialDate);
|
||||||
const [ztData, setZtData] = useState(null);
|
const [ztData, setZtData] = useState(null);
|
||||||
const [stats, setStats] = useState(null);
|
const [stats, setStats] = useState(null);
|
||||||
const [hotSectors, setHotSectors] = useState([]);
|
const [hotSectors, setHotSectors] = useState([]);
|
||||||
@@ -179,15 +189,6 @@ const MarketHot = ({ navigation }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack space={2}>
|
|
||||||
<Pressable
|
|
||||||
onPress={() => navigation.navigate('TodayStats')}
|
|
||||||
p={2}
|
|
||||||
rounded="full"
|
|
||||||
bg="rgba(255,255,255,0.1)"
|
|
||||||
>
|
|
||||||
<Icon as={Ionicons} name="stats-chart" size="md" color="white" />
|
|
||||||
</Pressable>
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => navigation.navigate('EventCalendar')}
|
onPress={() => navigation.navigate('EventCalendar')}
|
||||||
p={2}
|
p={2}
|
||||||
@@ -197,7 +198,6 @@ const MarketHot = ({ navigation }) => {
|
|||||||
<Icon as={Ionicons} name="calendar" size="md" color="white" />
|
<Icon as={Ionicons} name="calendar" size="md" color="white" />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</HStack>
|
</HStack>
|
||||||
</HStack>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 日期选择器 */}
|
{/* 日期选择器 */}
|
||||||
@@ -212,7 +212,7 @@ const MarketHot = ({ navigation }) => {
|
|||||||
<Icon as={Ionicons} name="chevron-back" size="sm" color="gray.400" />
|
<Icon as={Ionicons} name="chevron-back" size="sm" color="gray.400" />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<Pressable onPress={() => setCurrentDate(new Date())}>
|
<Pressable onPress={() => setCurrentDate(getInitialDate())}>
|
||||||
<VStack alignItems="center">
|
<VStack alignItems="center">
|
||||||
<Text fontSize="lg" fontWeight="bold" color="white">
|
<Text fontSize="lg" fontWeight="bold" color="white">
|
||||||
{formatDisplayDate(currentDate)}
|
{formatDisplayDate(currentDate)}
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ const TopStockItem = ({ stock, rank }) => (
|
|||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
|
|
||||||
const TodayStats = ({ navigation }) => {
|
const TodayStats = ({ navigation, isEmbedded = false }) => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
@@ -364,7 +364,7 @@ const TodayStats = ({ navigation }) => {
|
|||||||
<ScrollView
|
<ScrollView
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingTop: insets.top,
|
paddingTop: isEmbedded ? 0 : insets.top,
|
||||||
paddingBottom: insets.bottom + 20,
|
paddingBottom: insets.bottom + 20,
|
||||||
}}
|
}}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
@@ -376,7 +376,8 @@ const TodayStats = ({ navigation }) => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* 标题栏 */}
|
{/* 标题栏 - 仅在非嵌入模式下显示 */}
|
||||||
|
{!isEmbedded && (
|
||||||
<Box px={4} pt={4} pb={2}>
|
<Box px={4} pt={4} pb={2}>
|
||||||
<HStack alignItems="center" justifyContent="space-between">
|
<HStack alignItems="center" justifyContent="space-between">
|
||||||
<HStack alignItems="center">
|
<HStack alignItems="center">
|
||||||
@@ -423,6 +424,7 @@ const TodayStats = ({ navigation }) => {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 双圆环仪表盘 */}
|
{/* 双圆环仪表盘 */}
|
||||||
<Box mx={4} mt={4}>
|
<Box mx={4} mt={4}>
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
* 复用 Web 端的 API 逻辑,适配 React Native
|
* 复用 Web 端的 API 逻辑,适配 React Native
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
export const API_BASE_URL = 'https://api.valuefrontier.cn';
|
export const API_BASE_URL = 'https://api.valuefrontier.cn';
|
||||||
|
|
||||||
// 静态数据存储地址(腾讯 COS)
|
// 静态数据存储地址(腾讯 COS)
|
||||||
export const API_BASE = 'https://valuefrontier-1308417363.cos-website.ap-shanghai.myqcloud.com';
|
export const API_BASE = 'https://valuefrontier-1308417363.cos-website.ap-shanghai.myqcloud.com';
|
||||||
|
|
||||||
|
// Token 存储键名(与 authService 保持一致)
|
||||||
|
const ACCESS_TOKEN_KEY = '@auth_access_token';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用 API 请求函数
|
* 通用 API 请求函数
|
||||||
* @param {string} url - API 路径
|
* @param {string} url - API 路径
|
||||||
@@ -21,13 +26,24 @@ export const apiRequest = async (url, options = {}) => {
|
|||||||
console.log(`[API] ${method} ${fullUrl}`);
|
console.log(`[API] ${method} ${fullUrl}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(fullUrl, {
|
// 获取存储的 access_token
|
||||||
...options,
|
const accessToken = await AsyncStorage.getItem(ACCESS_TOKEN_KEY);
|
||||||
headers: {
|
|
||||||
|
// 构建请求头
|
||||||
|
const headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...options.headers,
|
...options.headers,
|
||||||
},
|
};
|
||||||
// 重要:携带 Cookie 以支持 Session 认证
|
|
||||||
|
// 如果有 token,添加到 Authorization header
|
||||||
|
if (accessToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(fullUrl, {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
// 保留 credentials 以支持 Cookie(作为备选认证方式)
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { apiRequest } from './api';
|
|||||||
const STORAGE_KEYS = {
|
const STORAGE_KEYS = {
|
||||||
USER_INFO: '@auth_user_info',
|
USER_INFO: '@auth_user_info',
|
||||||
IS_LOGGED_IN: '@auth_is_logged_in',
|
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.USER_INFO, JSON.stringify(response.user));
|
||||||
await AsyncStorage.setItem(STORAGE_KEYS.IS_LOGGED_IN, 'true');
|
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);
|
console.log('[AuthService] 登录成功,用户:', response.user.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,11 +133,25 @@ export const authService = {
|
|||||||
try {
|
try {
|
||||||
await AsyncStorage.removeItem(STORAGE_KEYS.USER_INFO);
|
await AsyncStorage.removeItem(STORAGE_KEYS.USER_INFO);
|
||||||
await AsyncStorage.removeItem(STORAGE_KEYS.IS_LOGGED_IN);
|
await AsyncStorage.removeItem(STORAGE_KEYS.IS_LOGGED_IN);
|
||||||
|
await AsyncStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AuthService] 清除本地状态失败:', error);
|
console.error('[AuthService] 清除本地状态失败:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取存储的 access_token
|
||||||
|
* @returns {Promise<string|null>}
|
||||||
|
*/
|
||||||
|
getAccessToken: async () => {
|
||||||
|
try {
|
||||||
|
return await AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[AuthService] 读取 token 失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取本地存储的用户信息
|
* 获取本地存储的用户信息
|
||||||
* @returns {Promise<object|null>} 用户信息
|
* @returns {Promise<object|null>} 用户信息
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const initialState = {
|
|||||||
popularKeywords: [],
|
popularKeywords: [],
|
||||||
// 主线/题材事件数据
|
// 主线/题材事件数据
|
||||||
mainlineData: [],
|
mainlineData: [],
|
||||||
mainlineGroupBy: 'lv1',
|
mainlineGroupBy: 'lv3',
|
||||||
// 当前事件详情
|
// 当前事件详情
|
||||||
currentEvent: null,
|
currentEvent: null,
|
||||||
// 加载状态
|
// 加载状态
|
||||||
|
|||||||
@@ -2618,7 +2618,21 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
assistant_message = response.choices[0].message
|
assistant_message = response.choices[0].message
|
||||||
logger.info(f"[Agent Stream] LLM 响应: finish_reason={response.choices[0].finish_reason}")
|
finish_reason = response.choices[0].finish_reason
|
||||||
|
logger.info(f"[Agent Stream] LLM 响应: finish_reason={finish_reason}")
|
||||||
|
|
||||||
|
# 详细日志:检查 vLLM 返回的内容
|
||||||
|
tool_calls_count = len(assistant_message.tool_calls) if assistant_message.tool_calls else 0
|
||||||
|
logger.info(f"[Agent Stream] tool_calls 数量: {tool_calls_count}, finish_reason: {finish_reason}")
|
||||||
|
|
||||||
|
if assistant_message.content:
|
||||||
|
content_preview = assistant_message.content[:500].replace('\n', ' ')
|
||||||
|
logger.info(f"[Agent Stream] content 预览: {content_preview}")
|
||||||
|
# 检查是否包含工具调用标签(vLLM 应该已解析,如果仍有标签说明解析失败)
|
||||||
|
if '<minimax:tool_call>' in assistant_message.content:
|
||||||
|
logger.warning(f"[Agent Stream] ⚠️ vLLM 未解析工具调用!finish_reason={finish_reason}")
|
||||||
|
logger.warning(f"[Agent Stream] ⚠️ 请检查 vLLM 启动参数: --tool-call-parser minimax_m2")
|
||||||
|
logger.warning(f"[Agent Stream] ⚠️ 将使用备用解析器处理...")
|
||||||
|
|
||||||
# 获取工具调用(优先使用原生 tool_calls,其次解析文本格式)
|
# 获取工具调用(优先使用原生 tool_calls,其次解析文本格式)
|
||||||
native_tool_calls = assistant_message.tool_calls or []
|
native_tool_calls = assistant_message.tool_calls or []
|
||||||
@@ -2633,7 +2647,8 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
|||||||
'```tool_call' in content or
|
'```tool_call' in content or
|
||||||
'"tool":' in content or
|
'"tool":' in content or
|
||||||
'DSML' in content or # DeepSeek DSML 格式
|
'DSML' in content or # DeepSeek DSML 格式
|
||||||
'|DSML|' in content # 全角竖线版本
|
'|DSML|' in content or # 全角竖线版本
|
||||||
|
'<minimax:tool_call>' in content # MiniMax 格式
|
||||||
)
|
)
|
||||||
if has_tool_markers:
|
if has_tool_markers:
|
||||||
logger.info(f"[Agent Stream] 尝试从文本内容解析工具调用")
|
logger.info(f"[Agent Stream] 尝试从文本内容解析工具调用")
|
||||||
@@ -3023,6 +3038,9 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
|||||||
yield self._format_sse("summary_chunk", {"content": final_summary})
|
yield self._format_sse("summary_chunk", {"content": final_summary})
|
||||||
logger.warning("[Summary] 使用降级方案")
|
logger.warning("[Summary] 使用降级方案")
|
||||||
|
|
||||||
|
# 过滤掉可能残留的工具调用标签(MiniMax 等模型可能在总结中输出工具调用)
|
||||||
|
final_summary = self._filter_tool_call_tags(final_summary)
|
||||||
|
|
||||||
# 发送完整的总结和元数据
|
# 发送完整的总结和元数据
|
||||||
yield self._format_sse("summary", {
|
yield self._format_sse("summary", {
|
||||||
"content": final_summary,
|
"content": final_summary,
|
||||||
@@ -3091,6 +3109,17 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
|||||||
"""格式化 SSE 消息"""
|
"""格式化 SSE 消息"""
|
||||||
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
|
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
|
||||||
|
|
||||||
|
def _filter_tool_call_tags(self, text: str) -> str:
|
||||||
|
"""过滤掉文本中的工具调用标签(总结阶段不应包含工具调用)"""
|
||||||
|
import re
|
||||||
|
# 过滤 MiniMax 格式
|
||||||
|
text = re.sub(r'<minimax:tool_call>.*?</minimax:tool_call>', '', text, flags=re.DOTALL)
|
||||||
|
# 过滤其他格式
|
||||||
|
text = re.sub(r'<tool_call>.*?</tool_call>', '', text, flags=re.DOTALL)
|
||||||
|
text = re.sub(r'```tool_call.*?```', '', text, flags=re.DOTALL)
|
||||||
|
text = re.sub(r'<[|\|]DSML[|\|]function_calls>.*?</[|\|]DSML[|\|]function_calls>', '', text, flags=re.DOTALL)
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
def _parse_text_tool_calls(self, content: str) -> List[Dict[str, Any]]:
|
def _parse_text_tool_calls(self, content: str) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
解析文本格式的工具调用
|
解析文本格式的工具调用
|
||||||
@@ -3099,6 +3128,7 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
|||||||
1. <tool_call> <function=xxx> <parameter=yyy> value </parameter> </function> </tool_call>
|
1. <tool_call> <function=xxx> <parameter=yyy> value </parameter> </function> </tool_call>
|
||||||
2. ```tool_call\n{"name": "xxx", "arguments": {...}}\n```
|
2. ```tool_call\n{"name": "xxx", "arguments": {...}}\n```
|
||||||
3. DeepSeek DSML 格式: <|DSML|function_calls> <|DSML|invoke name="xxx"> <|DSML|parameter name="yyy" string="true">value</|DSML|parameter> </|DSML|invoke> </|DSML|function_calls>
|
3. DeepSeek DSML 格式: <|DSML|function_calls> <|DSML|invoke name="xxx"> <|DSML|parameter name="yyy" string="true">value</|DSML|parameter> </|DSML|invoke> </|DSML|function_calls>
|
||||||
|
4. MiniMax 格式: <minimax:tool_call> <invoke name="xxx"> <parameter name="yyy">value</parameter> </invoke> </minimax:tool_call>
|
||||||
|
|
||||||
返回: [{"name": "tool_name", "arguments": {...}}, ...]
|
返回: [{"name": "tool_name", "arguments": {...}}, ...]
|
||||||
"""
|
"""
|
||||||
@@ -3106,6 +3136,47 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
|||||||
|
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
|
|
||||||
|
# 格式0 (优先): MiniMax <minimax:tool_call> 格式
|
||||||
|
# <minimax:tool_call> <invoke name="get_stock_basic_info"> <parameter name="stock_code">002916.SZ</parameter> </invoke> </minimax:tool_call>
|
||||||
|
minimax_pattern = r'<minimax:tool_call>(.*?)</minimax:tool_call>'
|
||||||
|
minimax_matches = re.findall(minimax_pattern, content, re.DOTALL)
|
||||||
|
|
||||||
|
for minimax_content in minimax_matches:
|
||||||
|
# 解析 invoke 标签 - 支持带引号和不带引号的 name
|
||||||
|
invoke_pattern = r'<invoke\s+name=(?:"([^"]+)"|([^>\s]+))>(.*?)</invoke>'
|
||||||
|
invoke_matches = re.findall(invoke_pattern, minimax_content, re.DOTALL)
|
||||||
|
|
||||||
|
for name_quoted, name_unquoted, params_str in invoke_matches:
|
||||||
|
func_name = name_quoted or name_unquoted
|
||||||
|
if not func_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
arguments = {}
|
||||||
|
# 解析参数: <parameter name="xxx">value</parameter> 或 <parameter name=xxx>value</parameter>
|
||||||
|
param_pattern = r'<parameter\s+name=(?:"([^"]+)"|([^>\s]+))>(.*?)</parameter>'
|
||||||
|
param_matches = re.findall(param_pattern, params_str, re.DOTALL)
|
||||||
|
|
||||||
|
for pname_quoted, pname_unquoted, param_value in param_matches:
|
||||||
|
param_name = pname_quoted or pname_unquoted
|
||||||
|
if not param_name:
|
||||||
|
continue
|
||||||
|
param_value = param_value.strip()
|
||||||
|
# 尝试解析 JSON 值,否则作为字符串
|
||||||
|
try:
|
||||||
|
arguments[param_name] = json.loads(param_value)
|
||||||
|
except:
|
||||||
|
arguments[param_name] = param_value
|
||||||
|
|
||||||
|
tool_calls.append({
|
||||||
|
"name": func_name,
|
||||||
|
"arguments": arguments
|
||||||
|
})
|
||||||
|
|
||||||
|
# 如果 MiniMax 格式解析到了工具调用,直接返回(优先级最高)
|
||||||
|
if tool_calls:
|
||||||
|
logger.info(f"[MiniMax Tool Call] 解析到 {len(tool_calls)} 个工具调用: {tool_calls}")
|
||||||
|
return tool_calls
|
||||||
|
|
||||||
# 格式1: <tool_call> 标签格式
|
# 格式1: <tool_call> 标签格式
|
||||||
# 例如: <tool_call> <function=get_stock_concepts> <parameter=seccode> 300274 </parameter> </function> </tool_call>
|
# 例如: <tool_call> <function=get_stock_concepts> <parameter=seccode> 300274 </parameter> </function> </tool_call>
|
||||||
pattern1 = r'<tool_call>\s*<function=(\w+)>(.*?)</function>\s*</tool_call>'
|
pattern1 = r'<tool_call>\s*<function=(\w+)>(.*?)</function>\s*</tool_call>'
|
||||||
|
|||||||
Reference in New Issue
Block a user