Compare commits
6 Commits
afe1180736
...
1ecd3e6d10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ecd3e6d10 | ||
|
|
59fdb150a9 | ||
|
|
293886b54a | ||
|
|
f304268af9 | ||
|
|
cbef50c3e5 | ||
|
|
9990d95e28 |
@@ -2,7 +2,13 @@
|
|||||||
// 桌面版用户菜单 - 头像点击跳转到订阅页面
|
// 桌面版用户菜单 - 头像点击跳转到订阅页面
|
||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { Tooltip, useColorModeValue } from '@chakra-ui/react';
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverArrow,
|
||||||
|
useColorModeValue
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import UserAvatar from './UserAvatar';
|
import UserAvatar from './UserAvatar';
|
||||||
import { TooltipContent } from '../../../Subscription/CrownTooltip';
|
import { TooltipContent } from '../../../Subscription/CrownTooltip';
|
||||||
@@ -19,33 +25,47 @@ const DesktopUserMenu = memo(({ user }) => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { subscriptionInfo } = useSubscription();
|
const { subscriptionInfo } = useSubscription();
|
||||||
|
|
||||||
const tooltipBg = useColorModeValue('white', 'gray.800');
|
const popoverBg = useColorModeValue('white', 'gray.800');
|
||||||
const tooltipBorderColor = useColorModeValue('gray.200', 'gray.600');
|
const popoverBorderColor = useColorModeValue('gray.200', 'gray.600');
|
||||||
|
|
||||||
const handleAvatarClick = () => {
|
const handleAvatarClick = () => {
|
||||||
navigate('/home/pages/account/subscription');
|
navigate('/home/pages/account/subscription');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Popover
|
||||||
label={<TooltipContent subscriptionInfo={subscriptionInfo} />}
|
trigger="hover"
|
||||||
placement="bottom"
|
placement="bottom-end"
|
||||||
hasArrow
|
openDelay={100}
|
||||||
bg={tooltipBg}
|
closeDelay={200}
|
||||||
borderRadius="lg"
|
gutter={8}
|
||||||
border="1px solid"
|
|
||||||
borderColor={tooltipBorderColor}
|
|
||||||
boxShadow="lg"
|
|
||||||
p={3}
|
|
||||||
>
|
>
|
||||||
<span>
|
<PopoverTrigger>
|
||||||
<UserAvatar
|
<span>
|
||||||
user={user}
|
<UserAvatar
|
||||||
|
user={user}
|
||||||
|
subscriptionInfo={subscriptionInfo}
|
||||||
|
onClick={handleAvatarClick}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
bg={popoverBg}
|
||||||
|
borderRadius="lg"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={popoverBorderColor}
|
||||||
|
boxShadow="lg"
|
||||||
|
p={3}
|
||||||
|
w="auto"
|
||||||
|
_focus={{ outline: 'none' }}
|
||||||
|
>
|
||||||
|
<PopoverArrow bg={popoverBg} />
|
||||||
|
<TooltipContent
|
||||||
subscriptionInfo={subscriptionInfo}
|
subscriptionInfo={subscriptionInfo}
|
||||||
onClick={handleAvatarClick}
|
onNavigate={handleAvatarClick}
|
||||||
/>
|
/>
|
||||||
</span>
|
</PopoverContent>
|
||||||
</Tooltip>
|
</Popover>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,31 @@ import PropTypes from 'prop-types';
|
|||||||
/**
|
/**
|
||||||
* Tooltip 内容组件 - 显示详细的会员信息
|
* Tooltip 内容组件 - 显示详细的会员信息
|
||||||
* 导出此组件供头像也使用相同的 Tooltip 内容
|
* 导出此组件供头像也使用相同的 Tooltip 内容
|
||||||
|
*
|
||||||
|
* @param {Object} subscriptionInfo - 订阅信息
|
||||||
|
* @param {Function} onNavigate - 点击跳转回调(可选)
|
||||||
*/
|
*/
|
||||||
export const TooltipContent = ({ subscriptionInfo }) => {
|
export const TooltipContent = ({ subscriptionInfo, onNavigate }) => {
|
||||||
const tooltipText = useColorModeValue('gray.700', 'gray.100');
|
const tooltipText = useColorModeValue('gray.700', 'gray.100');
|
||||||
const dividerColor = useColorModeValue('gray.200', 'gray.600');
|
const dividerColor = useColorModeValue('gray.200', 'gray.600');
|
||||||
const { type, days_left, is_active } = subscriptionInfo;
|
const linkColor = useColorModeValue('blue.500', 'blue.300');
|
||||||
|
const { type, days_left, is_active, end_date } = subscriptionInfo;
|
||||||
|
|
||||||
|
// 格式化到期日期
|
||||||
|
const formatEndDate = (dateStr) => {
|
||||||
|
if (!dateStr) return null;
|
||||||
|
try {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedEndDate = formatEndDate(end_date);
|
||||||
|
|
||||||
// 基础版用户
|
// 基础版用户
|
||||||
if (type === 'free') {
|
if (type === 'free') {
|
||||||
@@ -30,8 +50,16 @@ export const TooltipContent = ({ subscriptionInfo }) => {
|
|||||||
<Text fontSize="sm" color={tooltipText} opacity={0.8}>
|
<Text fontSize="sm" color={tooltipText} opacity={0.8}>
|
||||||
解锁更多高级功能
|
解锁更多高级功能
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color={tooltipText} opacity={0.6} textAlign="center" mt={1}>
|
<Text
|
||||||
点击头像升级会员
|
fontSize="xs"
|
||||||
|
color={linkColor}
|
||||||
|
textAlign="center"
|
||||||
|
mt={1}
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{ textDecoration: 'underline' }}
|
||||||
|
onClick={onNavigate}
|
||||||
|
>
|
||||||
|
点击升级会员
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
@@ -72,15 +100,28 @@ export const TooltipContent = ({ subscriptionInfo }) => {
|
|||||||
还有 <Text as="span" fontWeight="600" color={isUrgent ? 'red.500' : isWarning ? 'orange.500' : tooltipText}>{days_left}</Text> 天到期
|
还有 <Text as="span" fontWeight="600" color={isUrgent ? 'red.500' : isWarning ? 'orange.500' : tooltipText}>{days_left}</Text> 天到期
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text fontSize="xs" color={tooltipText} opacity={0.7} pl={6}>
|
{formattedEndDate && (
|
||||||
|
<Text fontSize="xs" color={tooltipText} opacity={0.8} pl={6}>
|
||||||
|
到期日:<Text as="span" fontWeight="500">{formattedEndDate}</Text>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text fontSize="xs" color={tooltipText} opacity={0.6} pl={6}>
|
||||||
享受全部高级功能
|
享受全部高级功能
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 操作提示 */}
|
{/* 操作提示 */}
|
||||||
<Text fontSize="xs" color={tooltipText} opacity={0.6} textAlign="center" mt={1}>
|
<Text
|
||||||
{isExpired || isUrgent ? '点击头像立即续费' : '点击头像管理订阅'}
|
fontSize="xs"
|
||||||
|
color={linkColor}
|
||||||
|
textAlign="center"
|
||||||
|
mt={1}
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{ textDecoration: 'underline' }}
|
||||||
|
onClick={onNavigate}
|
||||||
|
>
|
||||||
|
{isExpired || isUrgent ? '点击立即续费' : '点击管理订阅'}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
@@ -133,5 +174,7 @@ TooltipContent.propTypes = {
|
|||||||
type: PropTypes.oneOf(['free', 'pro', 'max']).isRequired,
|
type: PropTypes.oneOf(['free', 'pro', 'max']).isRequired,
|
||||||
days_left: PropTypes.number,
|
days_left: PropTypes.number,
|
||||||
is_active: PropTypes.bool,
|
is_active: PropTypes.bool,
|
||||||
|
end_date: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
onNavigate: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -751,10 +751,11 @@ const InfoModal = () => {
|
|||||||
|
|
||||||
{/* 操作提示 */}
|
{/* 操作提示 */}
|
||||||
<Box
|
<Box
|
||||||
pt={2}
|
pt={3}
|
||||||
borderTop="1px solid rgba(255,255,255,0.1)"
|
mt={2}
|
||||||
|
borderTop="1px solid rgba(255,215,0,0.2)"
|
||||||
>
|
>
|
||||||
<Text fontSize="11px" color="whiteAlpha.400" textAlign="center">
|
<Text fontSize="12px" color="yellow.300" textAlign="center" fontWeight="medium">
|
||||||
💡 点击事件卡片查看详情 · K线图支持滚轮缩放和拖动
|
💡 点击事件卡片查看详情 · K线图支持滚轮缩放和拖动
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -493,6 +493,9 @@ const CompactSearchBox = ({
|
|||||||
}}
|
}}
|
||||||
style={{ flex: 1, minWidth: isMobile ? 100 : 200 }}
|
style={{ flex: 1, minWidth: isMobile ? 100 : 200 }}
|
||||||
className="gold-placeholder"
|
className="gold-placeholder"
|
||||||
|
allowClear={{
|
||||||
|
clearIcon: <CloseCircleOutlined style={{ color: PROFESSIONAL_COLORS.text.muted, fontSize: 14 }} />
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
prefix={<SearchOutlined style={{ color: PROFESSIONAL_COLORS.gold[500] }} />}
|
prefix={<SearchOutlined style={{ color: PROFESSIONAL_COLORS.gold[500] }} />}
|
||||||
|
|||||||
@@ -340,6 +340,44 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 禁用未来日期
|
||||||
|
const disabledDate = (current) => {
|
||||||
|
return current && current > dayjs().endOf('day');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 禁用未来时间(精确到分钟)
|
||||||
|
const disabledTime = (current) => {
|
||||||
|
if (!current) return {};
|
||||||
|
|
||||||
|
const now = dayjs();
|
||||||
|
const isToday = current.isSame(now, 'day');
|
||||||
|
|
||||||
|
if (!isToday) return {};
|
||||||
|
|
||||||
|
const currentHour = now.hour();
|
||||||
|
const currentMinute = now.minute();
|
||||||
|
|
||||||
|
return {
|
||||||
|
disabledHours: () => {
|
||||||
|
const hours = [];
|
||||||
|
for (let i = currentHour + 1; i < 24; i++) {
|
||||||
|
hours.push(i);
|
||||||
|
}
|
||||||
|
return hours;
|
||||||
|
},
|
||||||
|
disabledMinutes: (selectedHour) => {
|
||||||
|
if (selectedHour === currentHour) {
|
||||||
|
const minutes = [];
|
||||||
|
for (let i = currentMinute + 1; i < 60; i++) {
|
||||||
|
minutes.push(i);
|
||||||
|
}
|
||||||
|
return minutes;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// "更多时间" 按钮内容
|
// "更多时间" 按钮内容
|
||||||
const customRangeContent = (
|
const customRangeContent = (
|
||||||
<div style={{ padding: 8 }}>
|
<div style={{ padding: 8 }}>
|
||||||
@@ -350,10 +388,12 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
|||||||
placeholder={['开始时间', '结束时间']}
|
placeholder={['开始时间', '结束时间']}
|
||||||
onChange={handleCustomRangeOk}
|
onChange={handleCustomRangeOk}
|
||||||
value={customRange}
|
value={customRange}
|
||||||
|
disabledDate={disabledDate}
|
||||||
|
disabledTime={disabledTime}
|
||||||
style={{ marginBottom: 8 }}
|
style={{ marginBottom: 8 }}
|
||||||
/>
|
/>
|
||||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
|
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
|
||||||
支持精确到分钟的时间范围选择
|
支持精确到分钟的时间范围选择(不能超过当前时间)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -260,24 +260,7 @@ const ConceptStatsPanel = ({ apiBaseUrl, onConceptClick }) => {
|
|||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
) : (
|
) : (
|
||||||
<VStack spacing={2} align="stretch" maxH="400px" overflowY="auto"
|
<VStack spacing={2} align="stretch">
|
||||||
css={{
|
|
||||||
'&::-webkit-scrollbar': {
|
|
||||||
width: '4px',
|
|
||||||
},
|
|
||||||
'&::-webkit-scrollbar-track': {
|
|
||||||
background: 'rgba(255,255,255,0.05)',
|
|
||||||
borderRadius: '10px',
|
|
||||||
},
|
|
||||||
'&::-webkit-scrollbar-thumb': {
|
|
||||||
background: 'rgba(255,255,255,0.2)',
|
|
||||||
borderRadius: '10px',
|
|
||||||
},
|
|
||||||
'&::-webkit-scrollbar-thumb:hover': {
|
|
||||||
background: 'rgba(255,255,255,0.3)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data?.map((item, index) => (
|
{data?.map((item, index) => (
|
||||||
<Box key={index}>
|
<Box key={index}>
|
||||||
{renderItem(item, index)}
|
{renderItem(item, index)}
|
||||||
|
|||||||
@@ -963,7 +963,7 @@ const ConceptCenter = () => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 右上角股票数量徽章 */}
|
{/* 右上角股票数量徽章 - 可点击 */}
|
||||||
<Badge
|
<Badge
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={3}
|
top={3}
|
||||||
@@ -978,6 +978,13 @@ const ConceptCenter = () => {
|
|||||||
fontWeight="medium"
|
fontWeight="medium"
|
||||||
border="1px solid"
|
border="1px solid"
|
||||||
borderColor="whiteAlpha.200"
|
borderColor="whiteAlpha.200"
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(139, 92, 246, 0.6)',
|
||||||
|
borderColor: 'purple.400',
|
||||||
|
}}
|
||||||
|
transition="all 0.2s"
|
||||||
|
onClick={(e) => handleViewStocks(e, concept)}
|
||||||
>
|
>
|
||||||
{concept.stock_count || 0} 只股票
|
{concept.stock_count || 0} 只股票
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -1233,9 +1240,15 @@ const ConceptCenter = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<HStack spacing={4} flexWrap="wrap">
|
<HStack spacing={4} flexWrap="wrap">
|
||||||
<HStack spacing={1}>
|
<HStack
|
||||||
|
spacing={1}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={(e) => handleViewStocks(e, concept)}
|
||||||
|
_hover={{ color: 'purple.300' }}
|
||||||
|
transition="color 0.2s"
|
||||||
|
>
|
||||||
<Icon as={FaChartLine} boxSize={4} color="purple.300" />
|
<Icon as={FaChartLine} boxSize={4} color="purple.300" />
|
||||||
<Text fontSize="sm" fontWeight="medium" color="whiteAlpha.800">
|
<Text fontSize="sm" fontWeight="medium" color="whiteAlpha.800" _groupHover={{ color: 'purple.300' }}>
|
||||||
{concept.stock_count || 0} 只股票
|
{concept.stock_count || 0} 只股票
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -1332,7 +1345,14 @@ const ConceptCenter = () => {
|
|||||||
))}
|
))}
|
||||||
{concept.stocks.length > 3 && (
|
{concept.stocks.length > 3 && (
|
||||||
<WrapItem>
|
<WrapItem>
|
||||||
<Text fontSize="xs" color="purple.300" fontWeight="medium">
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
color="purple.300"
|
||||||
|
fontWeight="medium"
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{ color: 'purple.200', textDecoration: 'underline' }}
|
||||||
|
onClick={(e) => handleViewStocks(e, concept)}
|
||||||
|
>
|
||||||
+{concept.stocks.length - 3}更多
|
+{concept.stocks.length - 3}更多
|
||||||
</Text>
|
</Text>
|
||||||
</WrapItem>
|
</WrapItem>
|
||||||
|
|||||||
@@ -86,15 +86,6 @@ const ConceptCard = ({ concept, tradingDate, onViewDetails }) => {
|
|||||||
{concept.concept}
|
{concept.concept}
|
||||||
</Text>
|
</Text>
|
||||||
<HStack spacing={2} flexWrap="wrap">
|
<HStack spacing={2} flexWrap="wrap">
|
||||||
<Badge
|
|
||||||
bg="rgba(168, 85, 247, 0.15)"
|
|
||||||
color="#A855F7"
|
|
||||||
borderWidth="1px"
|
|
||||||
borderColor="#A855F7"
|
|
||||||
fontSize="xs"
|
|
||||||
>
|
|
||||||
相关度: {concept.score.toFixed(2)}
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
<Badge
|
||||||
bg="rgba(20, 184, 166, 0.15)"
|
bg="rgba(20, 184, 166, 0.15)"
|
||||||
color="#14B8A6"
|
color="#14B8A6"
|
||||||
@@ -618,9 +609,6 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
|||||||
<VStack align="start" spacing={1}>
|
<VStack align="start" spacing={1}>
|
||||||
<Text fontSize="xl">{selectedConcept?.concept}</Text>
|
<Text fontSize="xl">{selectedConcept?.concept}</Text>
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
<Badge colorScheme="purple">
|
|
||||||
相关度: {selectedConcept?.score?.toFixed(2)}
|
|
||||||
</Badge>
|
|
||||||
<Badge colorScheme="teal">
|
<Badge colorScheme="teal">
|
||||||
{selectedConcept?.stock_count} 只股票
|
{selectedConcept?.stock_count} 只股票
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -620,13 +620,6 @@ export default function ProfilePage() {
|
|||||||
{user?.last_seen ? new Date(user.last_seen).toLocaleDateString() : '未知'}
|
{user?.last_seen ? new Date(user.last_seen).toLocaleDateString() : '未知'}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<HStack justify="space-between" w="full">
|
|
||||||
<Text fontSize="sm" color="gray.600">账户状态</Text>
|
|
||||||
<Badge colorScheme={user?.status === 'active' ? 'green' : 'gray'}>
|
|
||||||
{user?.status === 'active' ? '正常' : '未激活'}
|
|
||||||
</Badge>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
</VStack>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -392,7 +392,27 @@ const StockOverview = () => {
|
|||||||
leafDepth: 1,
|
leafDepth: 1,
|
||||||
roam: false,
|
roam: false,
|
||||||
breadcrumb: {
|
breadcrumb: {
|
||||||
show: false
|
show: true,
|
||||||
|
top: 10,
|
||||||
|
left: 10,
|
||||||
|
itemStyle: {
|
||||||
|
color: colorMode === 'dark' ? '#1a1a2e' : '#f0f0f0',
|
||||||
|
borderColor: colorMode === 'dark' ? goldColor : '#ccc',
|
||||||
|
borderWidth: 1,
|
||||||
|
shadowBlur: colorMode === 'dark' ? 5 : 0,
|
||||||
|
shadowColor: colorMode === 'dark' ? `${goldColor}40` : 'transparent',
|
||||||
|
textStyle: {
|
||||||
|
color: colorMode === 'dark' ? goldColor : '#333'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: colorMode === 'dark' ? goldColor : '#e0e0e0',
|
||||||
|
textStyle: {
|
||||||
|
color: colorMode === 'dark' ? '#0a0a0a' : '#333'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
levels: [
|
levels: [
|
||||||
|
|||||||
Reference in New Issue
Block a user