Compare commits
18 Commits
5aedde7528
...
feature_20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf4fdf6a68 | ||
|
|
34338373cd | ||
|
|
589e1c20f9 | ||
|
|
60e9a40a1f | ||
|
|
b8b24643fe | ||
|
|
e9e9ec9051 | ||
|
|
5b0e420770 | ||
|
|
93f43054fd | ||
|
|
101d042b0e | ||
|
|
a1aa6718e6 | ||
|
|
753727c1c0 | ||
|
|
afc92ee583 | ||
|
|
d825e4fe59 | ||
|
|
62cf0a6c7d | ||
|
|
805d446775 | ||
|
|
24ddfcd4b5 | ||
|
|
a90158239b | ||
|
|
a8d4245595 |
4
app.py
4
app.py
@@ -3490,11 +3490,11 @@ def get_wechat_h5_auth_url():
|
||||
# 编码回调地址
|
||||
redirect_uri = urllib.parse.quote_plus(WECHAT_REDIRECT_URI)
|
||||
|
||||
# 构建授权 URL(使用 snsapi_login 获取用户信息)
|
||||
# 构建授权 URL(使用 snsapi_userinfo 获取用户信息,仅限微信内 H5 使用)
|
||||
auth_url = (
|
||||
f"https://open.weixin.qq.com/connect/oauth2/authorize?"
|
||||
f"appid={WECHAT_APPID}&redirect_uri={redirect_uri}"
|
||||
f"&response_type=code&scope=snsapi_login&state={state}"
|
||||
f"&response_type=code&scope=snsapi_userinfo&state={state}"
|
||||
"#wechat_redirect"
|
||||
)
|
||||
|
||||
|
||||
@@ -63,6 +63,59 @@ const BytedeskWidget = ({
|
||||
bytedesk.init();
|
||||
widgetRef.current = bytedesk;
|
||||
|
||||
// ⚡ H5 端样式适配:使用 MutationObserver 立即应用样式(避免闪烁)
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
|
||||
const applyBytedeskStyles = () => {
|
||||
const allElements = document.querySelectorAll('body > div');
|
||||
allElements.forEach(el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
// 检查是否是右下角固定定位的元素(Bytedesk 按钮)
|
||||
if (style.position === 'fixed' && style.right && style.bottom) {
|
||||
const rightVal = parseInt(style.right);
|
||||
const bottomVal = parseInt(style.bottom);
|
||||
if (rightVal >= 0 && rightVal < 100 && bottomVal >= 0 && bottomVal < 100) {
|
||||
// H5 端设置按钮尺寸为 48x48(只执行一次)
|
||||
if (isMobile && !el.dataset.bytedeskStyled) {
|
||||
el.dataset.bytedeskStyled = 'true';
|
||||
const button = el.querySelector('button');
|
||||
if (button) {
|
||||
button.style.width = '48px';
|
||||
button.style.height = '48px';
|
||||
button.style.minWidth = '48px';
|
||||
button.style.minHeight = '48px';
|
||||
}
|
||||
}
|
||||
// 提示框 3 秒后隐藏(查找白色气泡框)
|
||||
const children = el.querySelectorAll('div');
|
||||
children.forEach(child => {
|
||||
if (child.dataset.bytedeskTooltip) return; // 已处理过
|
||||
const childStyle = window.getComputedStyle(child);
|
||||
// 白色背景的提示框
|
||||
if (childStyle.backgroundColor === 'rgb(255, 255, 255)') {
|
||||
child.dataset.bytedeskTooltip = 'true';
|
||||
setTimeout(() => {
|
||||
child.style.transition = 'opacity 0.3s';
|
||||
child.style.opacity = '0';
|
||||
setTimeout(() => child.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 立即执行一次
|
||||
applyBytedeskStyles();
|
||||
|
||||
// 监听 DOM 变化,新元素出现时立即应用样式
|
||||
const observer = new MutationObserver(applyBytedeskStyles);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// 5 秒后停止监听(避免性能问题)
|
||||
setTimeout(() => observer.disconnect(), 5000);
|
||||
|
||||
// ⚡ 屏蔽 STOMP WebSocket 错误日志(不影响功能)
|
||||
const originalConsoleError = console.error;
|
||||
console.error = function(...args) {
|
||||
|
||||
@@ -82,29 +82,9 @@ const CitedContent = ({
|
||||
...containerStyle
|
||||
}}
|
||||
>
|
||||
{/* AI 标识 - 固定在右上角 */}
|
||||
{showAIBadge && (
|
||||
<Tag
|
||||
icon={<RobotOutlined />}
|
||||
color="purple"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
margin: 0,
|
||||
zIndex: 10,
|
||||
fontSize: 12,
|
||||
padding: '2px 8px'
|
||||
}}
|
||||
className="ai-badge-responsive"
|
||||
>
|
||||
AI合成
|
||||
</Tag>
|
||||
)}
|
||||
|
||||
{/* 标题栏 */}
|
||||
{title && (
|
||||
<div style={{ marginBottom: 12, paddingRight: 80 }}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong style={{ fontSize: 14, color: finalTitleColor }}>
|
||||
{title}
|
||||
</Text>
|
||||
@@ -112,10 +92,24 @@ const CitedContent = ({
|
||||
)}
|
||||
|
||||
{/* 带引用的文本内容 */}
|
||||
<div style={{
|
||||
lineHeight: 1.8,
|
||||
paddingRight: title ? 0 : (showAIBadge ? 80 : 0)
|
||||
}}>
|
||||
<div style={{ lineHeight: 1.8 }}>
|
||||
{/* AI 标识 - 行内显示在文字前面 */}
|
||||
{showAIBadge && (
|
||||
<Tag
|
||||
icon={<RobotOutlined />}
|
||||
color="purple"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
padding: '2px 8px',
|
||||
marginRight: 8,
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-flex',
|
||||
}}
|
||||
className="ai-badge-responsive"
|
||||
>
|
||||
AI合成
|
||||
</Tag>
|
||||
)}
|
||||
{/* 前缀标签(如果有) */}
|
||||
{prefix && (
|
||||
<Text style={{
|
||||
|
||||
@@ -571,8 +571,8 @@ export const NotificationProvider = ({ children }) => {
|
||||
logger.info('NotificationContext', 'Auto-requesting browser permission on notification');
|
||||
await requestBrowserPermission();
|
||||
}
|
||||
// 如果权限是denied(已拒绝),提供设置指引
|
||||
else if (browserPermission === 'denied') {
|
||||
// 如果权限是denied(已拒绝),提供设置指引(仅 PC 端显示)
|
||||
else if (browserPermission === 'denied' && !isMobile) {
|
||||
const toastId = 'browser-permission-denied-guide';
|
||||
if (!toast.isActive(toastId)) {
|
||||
toast({
|
||||
|
||||
@@ -36,3 +36,37 @@ iframe[src*="/visitor/"] {
|
||||
[class*="bytedesk-badge"] {
|
||||
z-index: 1000000 !important;
|
||||
}
|
||||
|
||||
/* ========== H5 端客服组件整体缩小 ========== */
|
||||
@media (max-width: 768px) {
|
||||
/* 整个客服容器缩小(包括按钮和提示框) */
|
||||
[class*="bytedesk"],
|
||||
[id*="bytedesk"],
|
||||
[class*="BytedeskWeb"] {
|
||||
transform: scale(0.7) !important;
|
||||
transform-origin: bottom right !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 提示框 3 秒后自动消失 ========== */
|
||||
/* 提示框("在线客服 点击咨询"气泡)- 扩展选择器 */
|
||||
[class*="bytedesk-bubble"],
|
||||
[class*="bytedesk-tooltip"],
|
||||
[class*="BytedeskWeb"] [class*="bubble"],
|
||||
[class*="BytedeskWeb"] [class*="tooltip"],
|
||||
[class*="bytedesk"] > div:not(button):not(iframe),
|
||||
[class*="BytedeskWeb"] > div:not(button):not(iframe),
|
||||
[id*="bytedesk"] > div:not(button):not(iframe) {
|
||||
animation: bytedeskFadeOut 0.3s ease-out 3s forwards !important;
|
||||
}
|
||||
|
||||
@keyframes bytedeskFadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
80
src/views/Community/components/CompactSearchBox.css
Normal file
80
src/views/Community/components/CompactSearchBox.css
Normal file
@@ -0,0 +1,80 @@
|
||||
/* CompactSearchBox.css */
|
||||
/* 紧凑版搜索和筛选组件样式 */
|
||||
|
||||
/* 搜索框 placeholder 白色 - 全覆盖选择器 */
|
||||
.gold-placeholder input::placeholder,
|
||||
.gold-placeholder input[type="text"]::placeholder,
|
||||
.gold-placeholder .ant-input::placeholder,
|
||||
.gold-placeholder .ant-input-affix-wrapper input::placeholder,
|
||||
.gold-placeholder .ant-select-selection-search-input::placeholder,
|
||||
.gold-placeholder .ant-input-affix-wrapper .ant-input::placeholder {
|
||||
color: #FFFFFF !important;
|
||||
opacity: 0.8 !important;
|
||||
}
|
||||
|
||||
/* AutoComplete placeholder - 关键选择器 */
|
||||
.gold-placeholder .ant-select-selection-placeholder {
|
||||
color: #FFFFFF !important;
|
||||
opacity: 0.8 !important;
|
||||
}
|
||||
|
||||
.gold-placeholder .ant-input-affix-wrapper .ant-input,
|
||||
.gold-placeholder .ant-input {
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.gold-placeholder .ant-input-affix-wrapper {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 透明下拉框样式 */
|
||||
.transparent-select .ant-select-selector,
|
||||
.transparent-cascader .ant-select-selector {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* 行业筛选宽度自适应,减少间距 */
|
||||
.transparent-cascader {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.transparent-cascader .ant-select-selector {
|
||||
padding-right: 8px !important;
|
||||
min-width: unset !important;
|
||||
}
|
||||
|
||||
/* 行业筛选 Cascader placeholder 白色 */
|
||||
.transparent-select .ant-select-selection-placeholder,
|
||||
.transparent-cascader .ant-select-selection-placeholder,
|
||||
.transparent-cascader input::placeholder,
|
||||
.transparent-cascader .ant-cascader-input::placeholder {
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.transparent-cascader .ant-cascader-input {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 行业筛选 Cascader 选中值白色 */
|
||||
.transparent-cascader .ant-select-selection-item,
|
||||
.transparent-cascader .ant-cascader-picker-label {
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
/* 方括号样式下拉框 - 无边框 */
|
||||
.bracket-select .ant-select-selector {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.bracket-select .ant-select-selection-item,
|
||||
.bracket-select .ant-select-selection-placeholder {
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.bracket-select .ant-select-arrow {
|
||||
color: rgba(255, 255, 255, 0.65) !important;
|
||||
}
|
||||
@@ -4,30 +4,49 @@
|
||||
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Input, Cascader, Button, Space, Tag, AutoComplete, Select as AntSelect,
|
||||
Tooltip
|
||||
Tooltip, Divider, Flex
|
||||
} from 'antd';
|
||||
import {
|
||||
SearchOutlined, CloseCircleOutlined, StockOutlined, FilterOutlined,
|
||||
CalendarOutlined, SortAscendingOutlined
|
||||
CalendarOutlined, SortAscendingOutlined, ReloadOutlined, ThunderboltOutlined
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { fetchIndustryData, selectIndustryData, selectIndustryLoading } from '../../../store/slices/industrySlice';
|
||||
import { stockService } from '../../../services/stockService';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import { fetchIndustryData, selectIndustryData, selectIndustryLoading } from '@store/slices/industrySlice';
|
||||
import { stockService } from '@services/stockService';
|
||||
import { logger } from '@utils/logger';
|
||||
import TradingTimeFilter from './TradingTimeFilter';
|
||||
import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme';
|
||||
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
|
||||
import './CompactSearchBox.css';
|
||||
|
||||
const { Option } = AntSelect;
|
||||
|
||||
// 排序选项常量
|
||||
const SORT_OPTIONS = [
|
||||
{ value: 'new', label: '最新排序', mobileLabel: '最新' },
|
||||
{ value: 'hot', label: '最热排序', mobileLabel: '热门' },
|
||||
{ value: 'importance', label: '重要性排序', mobileLabel: '重要' },
|
||||
{ value: 'returns_avg', label: '平均收益', mobileLabel: '均收' },
|
||||
{ value: 'returns_week', label: '周收益', mobileLabel: '周收' },
|
||||
];
|
||||
|
||||
// 重要性等级常量
|
||||
const IMPORTANCE_OPTIONS = [
|
||||
{ value: 'S', label: 'S级' },
|
||||
{ value: 'A', label: 'A级' },
|
||||
{ value: 'B', label: 'B级' },
|
||||
{ value: 'C', label: 'C级' },
|
||||
];
|
||||
|
||||
const CompactSearchBox = ({
|
||||
onSearch,
|
||||
onSearchFocus,
|
||||
filters = {},
|
||||
mode,
|
||||
pageSize,
|
||||
trackingFunctions = {}
|
||||
trackingFunctions = {},
|
||||
isMobile = false
|
||||
}) => {
|
||||
// 状态
|
||||
const [stockOptions, setStockOptions] = useState([]);
|
||||
@@ -420,19 +439,21 @@ const CompactSearchBox = ({
|
||||
dispatch(fetchIndustryData());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: window.innerWidth < 768 ? '12px 16px' : '16px 20px',
|
||||
background: PROFESSIONAL_COLORS.background.card,
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3), 0 0 20px rgba(255, 195, 0, 0.1)',
|
||||
border: `1px solid ${PROFESSIONAL_COLORS.border.default}`,
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}>
|
||||
{/* 单行紧凑布局 - 移动端自动换行 */}
|
||||
<Space wrap style={{ width: '100%' }} size={window.innerWidth < 768 ? 'small' : 'medium'}>
|
||||
{/* 搜索框 */}
|
||||
<div style={{ padding: 0, background: 'transparent' }}>
|
||||
{/* 第一行:搜索框 + 日期筛选 */}
|
||||
<Flex
|
||||
align="center"
|
||||
gap={isMobile ? 8 : 12}
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.03)',
|
||||
border: `1px solid ${PROFESSIONAL_COLORS.border.light}`,
|
||||
borderRadius: '24px',
|
||||
padding: isMobile ? '2px 4px' : '8px 16px',
|
||||
marginBottom: isMobile ? 8 : 12
|
||||
}}
|
||||
>
|
||||
{/* 搜索框 - flex: 1 占满剩余空间 */}
|
||||
<AutoComplete
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
@@ -440,46 +461,57 @@ const CompactSearchBox = ({
|
||||
onSelect={handleStockSelect}
|
||||
onFocus={onSearchFocus}
|
||||
options={stockOptions}
|
||||
placeholder="搜索股票/话题..."
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleMainSearch();
|
||||
}
|
||||
}}
|
||||
style={{ width: window.innerWidth < 768 ? '100%' : 240, minWidth: window.innerWidth < 768 ? 0 : 240 }}
|
||||
style={{ flex: 1, minWidth: isMobile ? 100 : 200 }}
|
||||
className="gold-placeholder"
|
||||
>
|
||||
<Input
|
||||
prefix={<SearchOutlined style={{ color: PROFESSIONAL_COLORS.gold[500] }} />}
|
||||
placeholder="搜索股票/话题..."
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${PROFESSIONAL_COLORS.border.default}`,
|
||||
boxShadow: `0 2px 8px rgba(255, 195, 0, 0.1)`,
|
||||
background: PROFESSIONAL_COLORS.background.secondary,
|
||||
color: PROFESSIONAL_COLORS.text.primary
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: PROFESSIONAL_COLORS.text.primary,
|
||||
boxShadow: 'none'
|
||||
}}
|
||||
/>
|
||||
</AutoComplete>
|
||||
|
||||
{/* 时间筛选 */}
|
||||
<Tooltip title="时间筛选">
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
<CalendarOutlined style={{ color: PROFESSIONAL_COLORS.gold[500], fontSize: 12 }} />
|
||||
<TradingTimeFilter
|
||||
value={tradingTimeRange?.key || null}
|
||||
onChange={handleTradingTimeChange}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/* 分隔线 - H5 时隐藏 */}
|
||||
{!isMobile && <Divider type="vertical" style={{ height: 24, margin: '0 8px', borderColor: 'rgba(255,255,255,0.15)' }} />}
|
||||
|
||||
{/* 行业筛选 */}
|
||||
<Tooltip title="行业分类">
|
||||
{/* 日期筛选按钮组 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 0 }}>
|
||||
<CalendarOutlined style={{ color: PROFESSIONAL_COLORS.gold[500], fontSize: 14, marginRight: 8 }} />
|
||||
<TradingTimeFilter
|
||||
value={tradingTimeRange?.key || null}
|
||||
onChange={handleTradingTimeChange}
|
||||
compact={!isMobile}
|
||||
mobile={isMobile}
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
{/* 第二行:筛选条件 */}
|
||||
<Flex justify="space-between" align="center">
|
||||
{/* 左侧筛选 */}
|
||||
<Space size={isMobile ? 4 : 8}>
|
||||
{/* 行业筛选 */}
|
||||
<Cascader
|
||||
value={industryValue}
|
||||
onChange={handleIndustryChange}
|
||||
onFocus={handleCascaderFocus}
|
||||
options={industryData || []}
|
||||
placeholder="行业"
|
||||
placeholder={
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<FilterOutlined style={{ fontSize: 12 }} />
|
||||
{isMobile ? '行业' : '行业筛选'}
|
||||
</span>
|
||||
}
|
||||
changeOnSelect
|
||||
showSearch={{
|
||||
filter: (inputValue, path) =>
|
||||
@@ -489,145 +521,65 @@ const CompactSearchBox = ({
|
||||
}}
|
||||
allowClear
|
||||
expandTrigger="hover"
|
||||
displayRender={(labels) => labels[labels.length - 1] || '行业'}
|
||||
displayRender={(labels) => labels[labels.length - 1] || (isMobile ? '行业' : '行业筛选')}
|
||||
disabled={industryLoading}
|
||||
style={{
|
||||
width: window.innerWidth < 768 ? '100%' : 120,
|
||||
minWidth: window.innerWidth < 768 ? 0 : 120,
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
suffixIcon={<FilterOutlined style={{ fontSize: 14, color: PROFESSIONAL_COLORS.gold[500] }} />}
|
||||
style={{ minWidth: isMobile ? 70 : 80 }}
|
||||
suffixIcon={null}
|
||||
className="transparent-cascader"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* 重要性筛选 */}
|
||||
<Tooltip title="事件等级筛选">
|
||||
{/* 事件等级 */}
|
||||
<AntSelect
|
||||
mode="multiple"
|
||||
value={importance}
|
||||
onChange={handleImportanceChange}
|
||||
style={{
|
||||
width: window.innerWidth < 768 ? '100%' : 120,
|
||||
minWidth: window.innerWidth < 768 ? 0 : 120,
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
placeholder="事件等级"
|
||||
style={{ minWidth: isMobile ? 100 : 120 }}
|
||||
placeholder={
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<ThunderboltOutlined style={{ fontSize: 12 }} />
|
||||
{isMobile ? '等级' : '事件等级'}
|
||||
</span>
|
||||
}
|
||||
maxTagCount={0}
|
||||
maxTagPlaceholder={(omittedValues) => `已选 ${omittedValues.length} 项`}
|
||||
maxTagPlaceholder={(omittedValues) => isMobile ? `${omittedValues.length}项` : `已选 ${omittedValues.length} 项`}
|
||||
className="bracket-select"
|
||||
>
|
||||
<Option value="S">S级</Option>
|
||||
<Option value="A">A级</Option>
|
||||
<Option value="B">B级</Option>
|
||||
<Option value="C">C级</Option>
|
||||
{IMPORTANCE_OPTIONS.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
||||
))}
|
||||
</AntSelect>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
||||
{/* 排序 */}
|
||||
<Tooltip title="排序方式">
|
||||
{/* 右侧排序和重置 */}
|
||||
<Space size={isMobile ? 4 : 8}>
|
||||
{/* 排序 */}
|
||||
<AntSelect
|
||||
value={sort}
|
||||
onChange={handleSortChange}
|
||||
style={{
|
||||
width: window.innerWidth < 768 ? '100%' : 130,
|
||||
minWidth: window.innerWidth < 768 ? 0 : 130,
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
suffixIcon={<SortAscendingOutlined style={{ fontSize: 14, color: PROFESSIONAL_COLORS.gold[500] }} />}
|
||||
style={{ minWidth: isMobile ? 55 : 120 }}
|
||||
className="bracket-select"
|
||||
>
|
||||
<Option value="new">⏰ 最新</Option>
|
||||
<Option value="hot">🔥 最热</Option>
|
||||
<Option value="importance">⭐ 重要性</Option>
|
||||
<Option value="returns_avg">📊 平均收益</Option>
|
||||
<Option value="returns_week">📈 周收益</Option>
|
||||
{SORT_OPTIONS.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<SortAscendingOutlined style={{ fontSize: 12 }} />
|
||||
{isMobile ? opt.mobileLabel : opt.label}
|
||||
</span>
|
||||
</Option>
|
||||
))}
|
||||
</AntSelect>
|
||||
</Tooltip>
|
||||
|
||||
{/* 重置按钮 */}
|
||||
<Tooltip title="重置所有筛选">
|
||||
{/* 重置按钮 */}
|
||||
<Button
|
||||
icon={<CloseCircleOutlined />}
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReset}
|
||||
danger
|
||||
type="primary"
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(255, 77, 79, 0.2)'
|
||||
}}
|
||||
type="text"
|
||||
style={{ color: PROFESSIONAL_COLORS.text.secondary }}
|
||||
>
|
||||
重置
|
||||
{!isMobile && '重置筛选'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
||||
{/* 激活的筛选标签(如果有的话) */}
|
||||
{(inputValue || industryValue.length > 0 || importance.length > 0 || tradingTimeRange || sort !== 'new') && (
|
||||
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
{inputValue && (
|
||||
<Tag closable onClose={() => {
|
||||
setInputValue('');
|
||||
const params = buildFilterParams({ q: '' });
|
||||
triggerSearch(params);
|
||||
}} color="blue">
|
||||
搜索: {inputValue}
|
||||
</Tag>
|
||||
)}
|
||||
{tradingTimeRange && (
|
||||
<Tag closable onClose={() => {
|
||||
setTradingTimeRange(null);
|
||||
const params = buildFilterParams({
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
recent_days: ''
|
||||
});
|
||||
triggerSearch(params);
|
||||
}} color="green">
|
||||
{tradingTimeRange.label}
|
||||
</Tag>
|
||||
)}
|
||||
{industryValue.length > 0 && industryData && (
|
||||
<Tag closable onClose={() => {
|
||||
setIndustryValue([]);
|
||||
const params = buildFilterParams({ industry_code: '' });
|
||||
triggerSearch(params);
|
||||
}} color="orange">
|
||||
行业: {(() => {
|
||||
const findLabel = (code, data) => {
|
||||
for (const item of data) {
|
||||
if (code.startsWith(item.value)) {
|
||||
if (item.value === code) {
|
||||
return item.label;
|
||||
} else {
|
||||
return findLabel(code, item.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const lastLevelCode = industryValue[industryValue.length - 1];
|
||||
return findLabel(lastLevelCode, industryData);
|
||||
})()}
|
||||
</Tag>
|
||||
)}
|
||||
{importance.length > 0 && (
|
||||
<Tag closable onClose={() => {
|
||||
setImportance([]);
|
||||
const params = buildFilterParams({ importance: 'all' });
|
||||
triggerSearch(params);
|
||||
}} color="purple">
|
||||
重要性: {importance.map(imp => ({ 'S': '极高', 'A': '高', 'B': '中', 'C': '低' }[imp] || imp)).join(', ')}
|
||||
</Tag>
|
||||
)}
|
||||
{sort && sort !== 'new' && (
|
||||
<Tag closable onClose={() => {
|
||||
setSort('new');
|
||||
const params = buildFilterParams({ sort: 'new' });
|
||||
triggerSearch(params);
|
||||
}} color="cyan">
|
||||
排序: {({ 'hot': '最热', 'importance': '重要性', 'returns_avg': '平均收益', 'returns_week': '周收益' }[sort] || sort)}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -536,74 +536,52 @@ const [currentMode, setCurrentMode] = useState('vertical');
|
||||
position="relative"
|
||||
zIndex={1}
|
||||
pb={3}
|
||||
px={isMobile ? 2 : undefined}
|
||||
>
|
||||
<VStack spacing={3} align="stretch">
|
||||
{/* 第一行:标题 + 通知开关 + 更新时间 */}
|
||||
{/* 第一行:标题 + 模式切换 + 通知开关 + 更新时间 */}
|
||||
<Flex justify="space-between" align="center">
|
||||
{/* 左侧:标题 */}
|
||||
<Heading size="md" color={PROFESSIONAL_COLORS.text.primary}>
|
||||
<HStack spacing={2}>
|
||||
<TimeIcon color={PROFESSIONAL_COLORS.gold[500]} />
|
||||
<Text bgGradient={PROFESSIONAL_COLORS.gradients.gold} bgClip="text">实时要闻·动态追踪</Text>
|
||||
</HStack>
|
||||
</Heading>
|
||||
{/* 左侧:标题 + 模式切换按钮 */}
|
||||
<HStack spacing={4}>
|
||||
<Heading size={isMobile ? "sm" : "md"} color={PROFESSIONAL_COLORS.text.primary}>
|
||||
<HStack spacing={2}>
|
||||
<TimeIcon color={PROFESSIONAL_COLORS.gold[500]} />
|
||||
<Text bgGradient={PROFESSIONAL_COLORS.gradients.gold} bgClip="text">实时要闻·动态追踪</Text>
|
||||
</HStack>
|
||||
</Heading>
|
||||
{/* 模式切换按钮(移动端隐藏) */}
|
||||
{!isMobile && <ModeToggleButtons mode={mode} onModeChange={handleModeToggle} />}
|
||||
</HStack>
|
||||
|
||||
{/* 右侧:通知开关 + 更新时间 */}
|
||||
<HStack spacing={3}>
|
||||
{/* 通知开关 - 移动端隐藏 */}
|
||||
{!isMobile && (
|
||||
<Tooltip
|
||||
label={browserPermission === 'granted'
|
||||
? '浏览器通知已开启'
|
||||
: '开启实时推送通知'}
|
||||
placement="left"
|
||||
hasArrow
|
||||
<HStack
|
||||
spacing={2}
|
||||
cursor="pointer"
|
||||
onClick={handleNotificationToggle}
|
||||
_hover={{ opacity: 0.8 }}
|
||||
transition="opacity 0.2s"
|
||||
>
|
||||
<HStack
|
||||
spacing={2}
|
||||
px={3}
|
||||
py={1.5}
|
||||
borderRadius="md"
|
||||
bg={browserPermission === 'granted'
|
||||
? useColorModeValue('green.50', 'green.900')
|
||||
: useColorModeValue('gray.50', 'gray.700')}
|
||||
borderWidth="1px"
|
||||
borderColor={browserPermission === 'granted'
|
||||
? useColorModeValue('green.200', 'green.700')
|
||||
: useColorModeValue('gray.200', 'gray.600')}
|
||||
cursor="pointer"
|
||||
_hover={{
|
||||
borderColor: browserPermission === 'granted'
|
||||
? useColorModeValue('green.300', 'green.600')
|
||||
: useColorModeValue('blue.300', 'blue.600'),
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
onClick={handleNotificationToggle}
|
||||
<Icon
|
||||
as={BellIcon}
|
||||
boxSize={3.5}
|
||||
color={PROFESSIONAL_COLORS.gold[500]}
|
||||
/>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color={PROFESSIONAL_COLORS.text.secondary}
|
||||
>
|
||||
<Icon
|
||||
as={BellIcon}
|
||||
boxSize={3.5}
|
||||
color={browserPermission === 'granted'
|
||||
? useColorModeValue('green.600', 'green.300')
|
||||
: useColorModeValue('gray.500', 'gray.400')}
|
||||
/>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
fontWeight="medium"
|
||||
color={browserPermission === 'granted'
|
||||
? useColorModeValue('green.700', 'green.200')
|
||||
: useColorModeValue('gray.600', 'gray.300')}
|
||||
>
|
||||
{browserPermission === 'granted' ? '已开启' : '开启通知'}
|
||||
</Text>
|
||||
<Switch
|
||||
size="sm"
|
||||
isChecked={browserPermission === 'granted'}
|
||||
pointerEvents="none"
|
||||
colorScheme="green"
|
||||
/>
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
实时消息推送:{browserPermission === 'granted' ? '已开启' : '未开启'}
|
||||
</Text>
|
||||
<Switch
|
||||
size="sm"
|
||||
isChecked={browserPermission === 'granted'}
|
||||
pointerEvents="none"
|
||||
colorScheme="yellow"
|
||||
/>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* 更新时间 */}
|
||||
@@ -622,6 +600,7 @@ const [currentMode, setCurrentMode] = useState('vertical');
|
||||
mode={mode}
|
||||
pageSize={pageSize}
|
||||
trackingFunctions={trackingFunctions}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</Box>
|
||||
</VStack>
|
||||
@@ -631,43 +610,14 @@ const [currentMode, setCurrentMode] = useState('vertical');
|
||||
<CardBody
|
||||
ref={cardBodyRef}
|
||||
position="relative"
|
||||
pt={4}
|
||||
pt={0}
|
||||
px={0}
|
||||
mx={0}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
overflow="visible"
|
||||
zIndex={1}
|
||||
>
|
||||
{/* 顶部控制栏:模式切换按钮 + 分页控制器(移动端隐藏) */}
|
||||
{!isMobile && (
|
||||
<Box
|
||||
position="sticky"
|
||||
top="0"
|
||||
zIndex={10}
|
||||
bg={cardBg}
|
||||
py={2}
|
||||
mb={2}
|
||||
borderBottom="1px solid"
|
||||
borderColor={borderColor}
|
||||
mx={-6}
|
||||
px={6}
|
||||
boxShadow="sm"
|
||||
>
|
||||
<Flex justify="space-between" align="center" flexWrap="wrap" gap={2}>
|
||||
{/* 左侧:模式切换按钮 */}
|
||||
<ModeToggleButtons mode={mode} onModeChange={handleModeToggle} />
|
||||
|
||||
{/* 右侧:分页控制器(仅在纵向模式显示) */}
|
||||
{mode === 'vertical' && totalPages > 1 && (
|
||||
<PaginationControl
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChangeWithScroll}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 内容区域 - 撑满剩余高度 */}
|
||||
<Box flex="1" minH={0} position="relative">
|
||||
{/* Loading 蒙层 - 数据请求时显示 */}
|
||||
|
||||
@@ -17,7 +17,7 @@ const ModeToggleButtons = React.memo(({ mode, onModeChange }) => {
|
||||
colorScheme="blue"
|
||||
variant={mode === 'vertical' ? 'solid' : 'outline'}
|
||||
>
|
||||
纵向
|
||||
列表
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onModeChange('four-row')}
|
||||
|
||||
@@ -9,18 +9,12 @@ import {
|
||||
Center,
|
||||
Text,
|
||||
useBreakpointValue,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { InfoIcon } from '@chakra-ui/icons';
|
||||
import HorizontalDynamicNewsEventCard from '../EventCard/HorizontalDynamicNewsEventCard';
|
||||
import EventDetailScrollPanel from './EventDetailScrollPanel';
|
||||
import DynamicNewsDetailPanel from '../DynamicNewsDetail/DynamicNewsDetailPanel';
|
||||
import EventDetailModal from '../EventDetailModal';
|
||||
|
||||
/**
|
||||
* 纵向分栏模式布局
|
||||
@@ -165,20 +159,11 @@ const VerticalModeLayout = React.memo(({
|
||||
|
||||
{/* 移动端详情弹窗 */}
|
||||
{isMobile && (
|
||||
<Modal isOpen={isMobileModalOpen} onClose={onMobileModalClose} size="full" scrollBehavior="inside">
|
||||
<ModalOverlay bg="blackAlpha.800" backdropFilter="blur(10px)" />
|
||||
<ModalContent maxW="100vw" m={0} borderRadius={0}>
|
||||
<ModalHeader bg="gray.900" color="white" borderBottom="1px solid" borderColor="gray.700">
|
||||
{mobileSelectedEvent?.title || '事件详情'}
|
||||
</ModalHeader>
|
||||
<ModalCloseButton color="white" />
|
||||
<ModalBody p={0} bg="gray.900">
|
||||
{mobileSelectedEvent && (
|
||||
<DynamicNewsDetailPanel event={mobileSelectedEvent} showHeader={false} />
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<EventDetailModal
|
||||
open={isMobileModalOpen}
|
||||
onClose={onMobileModalClose}
|
||||
event={mobileSelectedEvent}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -47,7 +47,7 @@ const CompactMetaBar = ({ event, importance, isFollowing, followerCount, onToggl
|
||||
spacing={3}
|
||||
zIndex={1}
|
||||
>
|
||||
{/* 重要性徽章 - 与 EventHeaderInfo 样式一致,尺寸略小 */}
|
||||
{/* 重要性徽章 - 与 EventHeaderInfo 样式一致,尺寸略小 - H5 隐藏 */}
|
||||
<Badge
|
||||
px={3}
|
||||
py={1.5}
|
||||
@@ -62,7 +62,7 @@ const CompactMetaBar = ({ event, importance, isFollowing, followerCount, onToggl
|
||||
}
|
||||
color="white"
|
||||
boxShadow="lg"
|
||||
display="flex"
|
||||
display={{ base: 'none', lg: 'flex' }}
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
>
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
// 精简模式股票卡片组件(浮动卡片样式)
|
||||
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
Tooltip,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { getChangeColor, getChangeBackgroundGradient, getChangeBorderColor } from '../../../../utils/colorUtils';
|
||||
import { selectIsMobile } from '@store/slices/deviceSlice';
|
||||
import { getChangeColor, getChangeBackgroundGradient, getChangeBorderColor } from '@utils/colorUtils';
|
||||
|
||||
/**
|
||||
* 精简模式股票卡片组件
|
||||
@@ -17,6 +19,7 @@ import { getChangeColor, getChangeBackgroundGradient, getChangeBorderColor } fro
|
||||
* @param {Object} props.quote - 股票行情数据(可选)
|
||||
*/
|
||||
const CompactStockItem = ({ stock, quote = null }) => {
|
||||
const isMobile = useSelector(selectIsMobile);
|
||||
const nameColor = useColorModeValue('gray.700', 'gray.300');
|
||||
|
||||
const handleViewDetail = () => {
|
||||
@@ -45,10 +48,10 @@ const CompactStockItem = ({ stock, quote = null }) => {
|
||||
>
|
||||
<Box
|
||||
bgGradient={getChangeBackgroundGradient(change)}
|
||||
borderWidth="3px"
|
||||
borderWidth="1px"
|
||||
borderColor={getChangeBorderColor(change)}
|
||||
borderRadius="2xl"
|
||||
p={4}
|
||||
borderRadius="xl"
|
||||
p={2}
|
||||
onClick={handleViewDetail}
|
||||
cursor="pointer"
|
||||
boxShadow="lg"
|
||||
@@ -69,14 +72,14 @@ const CompactStockItem = ({ stock, quote = null }) => {
|
||||
}}
|
||||
transition="all 0.3s ease-in-out"
|
||||
display="inline-block"
|
||||
minW="150px"
|
||||
minW="100px"
|
||||
>
|
||||
{/* 股票代码 */}
|
||||
<Text
|
||||
fontSize="md"
|
||||
fontSize={isMobile ? "sm" : "md"}
|
||||
fontWeight="bold"
|
||||
color={getChangeColor(change)}
|
||||
mb={2}
|
||||
mb={isMobile ? 1 : 2}
|
||||
textAlign="center"
|
||||
>
|
||||
{stock.stock_code}
|
||||
@@ -84,7 +87,7 @@ const CompactStockItem = ({ stock, quote = null }) => {
|
||||
|
||||
{/* 涨跌幅 - 超大号显示 */}
|
||||
<Text
|
||||
fontSize="3xl"
|
||||
fontSize={isMobile ? "xl" : "3xl"}
|
||||
fontWeight="black"
|
||||
color={getChangeColor(change)}
|
||||
textAlign="center"
|
||||
@@ -96,9 +99,9 @@ const CompactStockItem = ({ stock, quote = null }) => {
|
||||
|
||||
{/* 股票名称(小字) */}
|
||||
<Text
|
||||
fontSize="xs"
|
||||
fontSize={isMobile ? "2xs" : "xs"}
|
||||
color={nameColor}
|
||||
mt={2}
|
||||
mt={isMobile ? 1 : 2}
|
||||
textAlign="center"
|
||||
noOfLines={1}
|
||||
fontWeight="medium"
|
||||
|
||||
@@ -418,7 +418,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* 讨论区(评论区) - 所有登录用户可用 */}
|
||||
<Box mt={4}>
|
||||
<Box>
|
||||
<EventCommentSection eventId={event.id} />
|
||||
</Box>
|
||||
</VStack>
|
||||
|
||||
@@ -37,16 +37,16 @@ const SimpleConceptCard = ({ concept, onClick, getRelevanceColor }) => {
|
||||
return (
|
||||
<VStack
|
||||
align="stretch"
|
||||
spacing={2}
|
||||
spacing={1}
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
px={4}
|
||||
py={2}
|
||||
px={2}
|
||||
py={1}
|
||||
cursor="pointer"
|
||||
transition="all 0.2s"
|
||||
minW="200px"
|
||||
minW="100px"
|
||||
_hover={{
|
||||
transform: 'translateY(-1px)',
|
||||
boxShadow: 'md',
|
||||
@@ -68,17 +68,17 @@ const SimpleConceptCard = ({ concept, onClick, getRelevanceColor }) => {
|
||||
</Text>
|
||||
|
||||
{/* 第二行:相关度 + 涨跌幅 */}
|
||||
<Flex justify="space-between" align="center" gap={2} flexWrap="wrap">
|
||||
<Flex justify="space-between" align="center" gap={1} flexWrap="wrap">
|
||||
{/* 相关度标签 */}
|
||||
<Box
|
||||
bg={relevanceColors.bg}
|
||||
color={relevanceColors.color}
|
||||
px={2}
|
||||
px={1.5}
|
||||
py={0.5}
|
||||
borderRadius="sm"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text fontSize="xs" fontWeight="medium" whiteSpace="nowrap">
|
||||
<Text fontSize="10px" fontWeight="medium" whiteSpace="nowrap">
|
||||
相关度: {relevanceScore}%
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -87,8 +87,8 @@ const SimpleConceptCard = ({ concept, onClick, getRelevanceColor }) => {
|
||||
{changePct !== null && (
|
||||
<Badge
|
||||
colorScheme={changeColor}
|
||||
fontSize="xs"
|
||||
px={2}
|
||||
fontSize="10px"
|
||||
px={1.5}
|
||||
py={0.5}
|
||||
flexShrink={0}
|
||||
>
|
||||
|
||||
@@ -243,7 +243,7 @@ const RelatedConceptsSection = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isExpanded ? '收起' : '查看详细描述'}
|
||||
{isExpanded ? '收起' : '查看详细'}
|
||||
</Button>
|
||||
</Flex>
|
||||
{/* 第二行:交易日期信息 */}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 股票卡片组件(融合表格功能的卡片样式)
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -16,13 +17,15 @@ import {
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { StarIcon } from '@chakra-ui/icons';
|
||||
import { Tag } from 'antd';
|
||||
import { RobotOutlined } from '@ant-design/icons';
|
||||
import { selectIsMobile } from '@store/slices/deviceSlice';
|
||||
import MiniTimelineChart from '../StockDetailPanel/components/MiniTimelineChart';
|
||||
import MiniKLineChart from './MiniKLineChart';
|
||||
import TimelineChartModal from '../../../../components/StockChart/TimelineChartModal';
|
||||
import KLineChartModal from '../../../../components/StockChart/KLineChartModal';
|
||||
import CitedContent from '../../../../components/Citation/CitedContent';
|
||||
import { getChangeColor } from '../../../../utils/colorUtils';
|
||||
import { PROFESSIONAL_COLORS } from '../../../../constants/professionalTheme';
|
||||
import TimelineChartModal from '@components/StockChart/TimelineChartModal';
|
||||
import KLineChartModal from '@components/StockChart/KLineChartModal';
|
||||
import { getChangeColor } from '@utils/colorUtils';
|
||||
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
|
||||
|
||||
/**
|
||||
* 股票卡片组件
|
||||
@@ -44,6 +47,7 @@ const StockListItem = ({
|
||||
isInWatchlist = false,
|
||||
onWatchlistToggle
|
||||
}) => {
|
||||
const isMobile = useSelector(selectIsMobile);
|
||||
const cardBg = PROFESSIONAL_COLORS.background.card;
|
||||
const borderColor = PROFESSIONAL_COLORS.border.default;
|
||||
const codeColor = '#3B82F6';
|
||||
@@ -128,9 +132,9 @@ const StockListItem = ({
|
||||
transition="all 0.2s"
|
||||
>
|
||||
{/* 单行紧凑布局:名称+涨跌幅 | 分时图 | K线图 | 关联描述 */}
|
||||
<HStack spacing={2} align="center" flexWrap="wrap">
|
||||
<HStack spacing={2} align="center" flexWrap={isMobile ? 'wrap' : 'nowrap'}>
|
||||
{/* 左侧:股票信息区 */}
|
||||
<HStack spacing={2} minW="360px" maxW="380px" flexShrink={0}>
|
||||
<HStack spacing={2} overflow="hidden">
|
||||
{/* 股票代码 + 名称 + 涨跌幅 */}
|
||||
<VStack
|
||||
align="stretch"
|
||||
@@ -194,24 +198,24 @@ const StockListItem = ({
|
||||
</HStack>
|
||||
</VStack>
|
||||
|
||||
{/* 分时图 - 更紧凑 */}
|
||||
{/* 分时图 - 自适应 */}
|
||||
<VStack
|
||||
w="115px"
|
||||
flex={1}
|
||||
minW="80px"
|
||||
maxW="150px"
|
||||
borderWidth="1px"
|
||||
borderColor="rgba(59, 130, 246, 0.3)"
|
||||
borderRadius="md"
|
||||
px={1.5}
|
||||
py={1}
|
||||
px={2}
|
||||
py={1.5}
|
||||
bg="rgba(59, 130, 246, 0.1)"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsTimelineModalOpen(true);
|
||||
}}
|
||||
cursor="pointer"
|
||||
flexShrink={0}
|
||||
align="stretch"
|
||||
spacing={0}
|
||||
h="fit-content"
|
||||
_hover={{
|
||||
borderColor: '#3B82F6',
|
||||
boxShadow: '0 0 10px rgba(59, 130, 246, 0.3)',
|
||||
@@ -228,7 +232,7 @@ const StockListItem = ({
|
||||
>
|
||||
📈 分时
|
||||
</Text>
|
||||
<Box h="32px">
|
||||
<Box h="28px">
|
||||
<MiniTimelineChart
|
||||
stockCode={stock.stock_code}
|
||||
eventTime={eventTime}
|
||||
@@ -236,24 +240,24 @@ const StockListItem = ({
|
||||
</Box>
|
||||
</VStack>
|
||||
|
||||
{/* K线图 - 更紧凑 */}
|
||||
{/* K线图 - 自适应 */}
|
||||
<VStack
|
||||
w="115px"
|
||||
flex={1}
|
||||
minW="80px"
|
||||
maxW="150px"
|
||||
borderWidth="1px"
|
||||
borderColor="rgba(168, 85, 247, 0.3)"
|
||||
borderRadius="md"
|
||||
px={1.5}
|
||||
py={1}
|
||||
px={2}
|
||||
py={1.5}
|
||||
bg="rgba(168, 85, 247, 0.1)"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsKLineModalOpen(true);
|
||||
}}
|
||||
cursor="pointer"
|
||||
flexShrink={0}
|
||||
align="stretch"
|
||||
spacing={0}
|
||||
h="fit-content"
|
||||
_hover={{
|
||||
borderColor: '#A855F7',
|
||||
boxShadow: '0 0 10px rgba(168, 85, 247, 0.3)',
|
||||
@@ -270,7 +274,7 @@ const StockListItem = ({
|
||||
>
|
||||
📊 日线
|
||||
</Text>
|
||||
<Box h="32px">
|
||||
<Box h="28px">
|
||||
<MiniKLineChart
|
||||
stockCode={stock.stock_code}
|
||||
eventTime={eventTime}
|
||||
@@ -281,7 +285,7 @@ const StockListItem = ({
|
||||
|
||||
{/* 关联描述 - 升级和降级处理 */}
|
||||
{stock.relation_desc && (
|
||||
<Box flex={1} minW={0}>
|
||||
<Box flex={1} minW={0} flexBasis={isMobile ? '100%' : ''}>
|
||||
{stock.relation_desc?.data ? (
|
||||
// 升级:带引用来源的版本 - 添加折叠功能
|
||||
<Tooltip
|
||||
@@ -298,8 +302,6 @@ const StockListItem = ({
|
||||
setIsDescExpanded(!isDescExpanded);
|
||||
}}
|
||||
cursor="pointer"
|
||||
px={3}
|
||||
py={2}
|
||||
bg={PROFESSIONAL_COLORS.background.secondary}
|
||||
borderRadius="md"
|
||||
_hover={{
|
||||
@@ -308,18 +310,30 @@ const StockListItem = ({
|
||||
transition="background 0.2s"
|
||||
position="relative"
|
||||
>
|
||||
<Collapse in={isDescExpanded} startingHeight={40}>
|
||||
<CitedContent
|
||||
data={stock.relation_desc}
|
||||
title=""
|
||||
showAIBadge={true}
|
||||
textColor={PROFESSIONAL_COLORS.text.primary}
|
||||
containerStyle={{
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: '0',
|
||||
padding: '0',
|
||||
<Collapse in={isDescExpanded} startingHeight={56}>
|
||||
{/* AI 标识 - 行内显示在文字前面 */}
|
||||
<Tag
|
||||
icon={<RobotOutlined />}
|
||||
color="purple"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
padding: '2px 8px',
|
||||
marginRight: 8,
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-flex',
|
||||
}}
|
||||
/>
|
||||
>
|
||||
AI合成
|
||||
</Tag>
|
||||
{/* 直接渲染文字内容 */}
|
||||
<Text
|
||||
as="span"
|
||||
fontSize="sm"
|
||||
color={PROFESSIONAL_COLORS.text.primary}
|
||||
lineHeight="1.8"
|
||||
>
|
||||
{stock.relation_desc?.data?.map(item => item.sentences || item.query_part).filter(Boolean).join(',')}
|
||||
</Text>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
@@ -339,8 +353,6 @@ const StockListItem = ({
|
||||
setIsDescExpanded(!isDescExpanded);
|
||||
}}
|
||||
cursor="pointer"
|
||||
px={3}
|
||||
py={2}
|
||||
bg={PROFESSIONAL_COLORS.background.secondary}
|
||||
borderRadius="md"
|
||||
_hover={{
|
||||
@@ -350,7 +362,7 @@ const StockListItem = ({
|
||||
position="relative"
|
||||
>
|
||||
{/* 去掉"关联描述"标题 */}
|
||||
<Collapse in={isDescExpanded} startingHeight={36}>
|
||||
<Collapse in={isDescExpanded} startingHeight={56}>
|
||||
<Text
|
||||
fontSize="xs"
|
||||
color={nameColor}
|
||||
|
||||
@@ -32,10 +32,10 @@ const EventFollowButton = ({
|
||||
size={size}
|
||||
colorScheme="yellow"
|
||||
variant="ghost"
|
||||
bg="whiteAlpha.500"
|
||||
bg="rgba(113, 128, 150, 0.6)"
|
||||
boxShadow="sm"
|
||||
_hover={{
|
||||
bg: 'whiteAlpha.800',
|
||||
bg: 'rgba(113, 128, 150, 0.8)',
|
||||
boxShadow: 'md'
|
||||
}}
|
||||
icon={
|
||||
@@ -47,8 +47,7 @@ const EventFollowButton = ({
|
||||
) : (
|
||||
<AiOutlineStar
|
||||
size={iconSize}
|
||||
color="#718096"
|
||||
strokeWidth="1"
|
||||
color="gold"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
36
src/views/Community/components/EventDetailModal.less
Normal file
36
src/views/Community/components/EventDetailModal.less
Normal file
@@ -0,0 +1,36 @@
|
||||
.event-detail-modal {
|
||||
top: 20% !important;
|
||||
margin: 0 auto !important;
|
||||
padding-bottom: 0 !important;
|
||||
|
||||
.ant-modal-content {
|
||||
border-radius: 24px !important;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// 标题样式 - 深色文字(白色背景)
|
||||
.ant-modal-title {
|
||||
color: #1A202C;
|
||||
}
|
||||
|
||||
// 关闭按钮样式 - 深色(白色背景)
|
||||
.ant-modal-close {
|
||||
color: #4A5568;
|
||||
|
||||
&:hover {
|
||||
color: #1A202C;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自底向上滑入动画
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
48
src/views/Community/components/EventDetailModal.tsx
Normal file
48
src/views/Community/components/EventDetailModal.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Modal } from 'antd';
|
||||
import { selectIsMobile } from '@store/slices/deviceSlice';
|
||||
import DynamicNewsDetailPanel from './DynamicNewsDetail/DynamicNewsDetailPanel';
|
||||
import './EventDetailModal.less';
|
||||
|
||||
interface EventDetailModalProps {
|
||||
/** 是否打开弹窗 */
|
||||
open: boolean;
|
||||
/** 关闭弹窗回调 */
|
||||
onClose: () => void;
|
||||
/** 事件对象 */
|
||||
event: any; // TODO: 后续可替换为具体的 Event 类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件详情弹窗组件
|
||||
*/
|
||||
const EventDetailModal: React.FC<EventDetailModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
event,
|
||||
}) => {
|
||||
const isMobile = useSelector(selectIsMobile);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
title={event?.title || '事件详情'}
|
||||
width='100vw'
|
||||
destroyOnClose
|
||||
className="event-detail-modal"
|
||||
styles={{
|
||||
mask: { background: 'transparent' },
|
||||
content: { borderRadius: 24, padding: 0, maxWidth: 1400, background: 'transparent', margin: '0 auto' },
|
||||
header: { background: '#FFFFFF', borderBottom: '1px solid #E2E8F0', padding: '16px 24px', borderRadius: '24px 24px 0 0', margin: 0 },
|
||||
body: { padding: 0 },
|
||||
}}
|
||||
>
|
||||
{event && <DynamicNewsDetailPanel event={event} showHeader={false} />}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventDetailModal;
|
||||
@@ -2,19 +2,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Badge, Tag, Empty, Carousel, Tooltip } from 'antd';
|
||||
import { ArrowUpOutlined, ArrowDownOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import EventDetailModal from './EventDetailModal';
|
||||
import dayjs from 'dayjs';
|
||||
import './HotEvents.css';
|
||||
import defaultEventImage from '../../../assets/img/default-event.jpg';
|
||||
import DynamicNewsDetailPanel from './DynamicNewsDetail';
|
||||
|
||||
// 自定义箭头组件
|
||||
const CustomArrow = ({ className, style, onClick, direction }) => {
|
||||
@@ -196,21 +188,12 @@ const HotEvents = ({ events, onPageChange, onEventClick }) => {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 事件详情弹窗 - 使用 Chakra UI Modal(与平铺模式一致) */}
|
||||
{isModalOpen ? (
|
||||
<Modal isOpen={isModalOpen} onClose={onModalClose} size="6xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
{modalEvent?.title || '事件详情'}
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody pb={6}>
|
||||
{modalEvent && <DynamicNewsDetailPanel event={modalEvent} />}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
): null}
|
||||
{/* 事件详情弹窗 */}
|
||||
<EventDetailModal
|
||||
open={isModalOpen}
|
||||
onClose={onModalClose}
|
||||
event={modalEvent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
// src/views/Community/components/TradingTimeFilter.js
|
||||
// 交易时段智能筛选组件
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { Space, Button, Tag, Tooltip, DatePicker, Popover } from 'antd';
|
||||
import { ClockCircleOutlined, CalendarOutlined } from '@ant-design/icons';
|
||||
import { Space, Button, Tag, Tooltip, DatePicker, Popover, Select } from 'antd';
|
||||
import { ClockCircleOutlined, CalendarOutlined, FilterOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import locale from 'antd/es/date-picker/locale/zh_CN';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import { logger } from '@utils/logger';
|
||||
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
/**
|
||||
* 交易时段筛选组件
|
||||
* @param {string} value - 当前选中的 key(受控)
|
||||
* @param {Function} onChange - 时间范围变化回调 (timeConfig) => void
|
||||
* @param {boolean} compact - 是否使用紧凑模式(PC 端搜索栏内使用)
|
||||
* @param {boolean} mobile - 是否使用移动端模式(下拉选择)
|
||||
*/
|
||||
const TradingTimeFilter = ({ value, onChange }) => {
|
||||
const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false }) => {
|
||||
const [selectedKey, setSelectedKey] = useState(null);
|
||||
const [customRangeVisible, setCustomRangeVisible] = useState(false);
|
||||
const [customRange, setCustomRange] = useState(null);
|
||||
@@ -266,7 +271,39 @@ const TradingTimeFilter = ({ value, onChange }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染按钮
|
||||
// 渲染紧凑模式按钮(PC 端搜索栏内使用,文字按钮 + | 分隔符)
|
||||
const renderCompactButton = (config, showDivider = true) => {
|
||||
const isSelected = selectedKey === config.key;
|
||||
const fullTooltip = config.timeHint ? `${config.tooltip} · ${config.timeHint}` : config.tooltip;
|
||||
|
||||
return (
|
||||
<React.Fragment key={config.key}>
|
||||
<Tooltip title={fullTooltip}>
|
||||
<span
|
||||
onClick={() => handleButtonClick(config)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '13px',
|
||||
fontWeight: isSelected ? 600 : 400,
|
||||
color: isSelected ? PROFESSIONAL_COLORS.gold[500] : PROFESSIONAL_COLORS.text.secondary,
|
||||
background: isSelected ? 'rgba(255, 195, 0, 0.15)' : 'transparent',
|
||||
transition: 'all 0.2s ease',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{showDivider && (
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.2)', margin: '0 2px' }}>|</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染按钮(默认模式)
|
||||
const renderButton = (config) => {
|
||||
const isSelected = selectedKey === config.key;
|
||||
|
||||
@@ -321,6 +358,98 @@ const TradingTimeFilter = ({ value, onChange }) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
// 移动端模式:下拉选择器
|
||||
if (mobile) {
|
||||
const allButtons = [...timeRangeConfig.dynamic, ...timeRangeConfig.fixed];
|
||||
|
||||
const handleMobileSelect = (key) => {
|
||||
if (key === selectedKey) {
|
||||
// 取消选中
|
||||
setSelectedKey(null);
|
||||
onChange(null);
|
||||
} else {
|
||||
const config = allButtons.find(b => b.key === key);
|
||||
if (config) {
|
||||
handleButtonClick(config);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={selectedKey}
|
||||
onChange={handleMobileSelect}
|
||||
placeholder={
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<FilterOutlined style={{ fontSize: 12 }} />
|
||||
筛选
|
||||
</span>
|
||||
}
|
||||
allowClear
|
||||
onClear={() => {
|
||||
setSelectedKey(null);
|
||||
onChange(null);
|
||||
}}
|
||||
style={{ minWidth: 80 }}
|
||||
className="transparent-select"
|
||||
popupMatchSelectWidth={false}
|
||||
>
|
||||
{allButtons.map(config => (
|
||||
<Option key={config.key} value={config.key}>
|
||||
{config.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
// 紧凑模式:PC 端搜索栏内的样式
|
||||
if (compact) {
|
||||
// 合并所有按钮配置
|
||||
const allButtons = [...timeRangeConfig.dynamic, ...timeRangeConfig.fixed];
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'nowrap' }}>
|
||||
{allButtons.map((config, index) =>
|
||||
renderCompactButton(config, index < allButtons.length - 1)
|
||||
)}
|
||||
{/* 更多时间 */}
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.2)', margin: '0 2px' }}>|</span>
|
||||
<Popover
|
||||
content={customRangeContent}
|
||||
title="选择自定义时间范围"
|
||||
trigger="click"
|
||||
open={customRangeVisible}
|
||||
onOpenChange={setCustomRangeVisible}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<Tooltip title="自定义时间范围">
|
||||
<span
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '13px',
|
||||
fontWeight: selectedKey === 'custom' ? 600 : 400,
|
||||
color: selectedKey === 'custom' ? PROFESSIONAL_COLORS.gold[500] : PROFESSIONAL_COLORS.text.secondary,
|
||||
background: selectedKey === 'custom' ? 'rgba(255, 195, 0, 0.15)' : 'transparent',
|
||||
transition: 'all 0.2s ease',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<CalendarOutlined style={{ fontSize: 12 }} />
|
||||
更多
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 默认模式:移动端/独立使用
|
||||
return (
|
||||
<Space wrap size={[8, 8]} style={{ display: 'flex', alignItems: 'flex-start' }}>
|
||||
{/* 动态按钮(根据时段显示多个) */}
|
||||
|
||||
@@ -29,10 +29,12 @@ import {
|
||||
FaChartLine,
|
||||
FaInfoCircle
|
||||
} from 'react-icons/fa';
|
||||
import { stockService } from '../../../services/eventService';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import CitedContent from '../../../components/Citation/CitedContent';
|
||||
import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme';
|
||||
import { Tag } from 'antd';
|
||||
import { RobotOutlined } from '@ant-design/icons';
|
||||
import { stockService } from '@services/eventService';
|
||||
import { logger } from '@utils/logger';
|
||||
import CitedContent from '@components/Citation/CitedContent';
|
||||
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
|
||||
|
||||
const HistoricalEvents = ({
|
||||
events = [],
|
||||
@@ -244,7 +246,7 @@ const HistoricalEvents = ({
|
||||
key={event.id}
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderColor="gray.500"
|
||||
borderRadius="lg"
|
||||
position="relative"
|
||||
overflow="visible"
|
||||
@@ -267,16 +269,16 @@ const HistoricalEvents = ({
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<VStack align="stretch" spacing={2} p={3}>
|
||||
<VStack align="stretch" spacing={3} p={4}>
|
||||
{/* 顶部区域:左侧(标题+时间) + 右侧(按钮) */}
|
||||
<HStack align="flex-start" spacing={3}>
|
||||
{/* 左侧:标题 + 时间信息(允许折行) */}
|
||||
<VStack flex="1" align="flex-start" spacing={1}>
|
||||
<VStack flex="1" align="flex-start" spacing={2}>
|
||||
{/* 标题 */}
|
||||
<Text
|
||||
fontSize="md"
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
color={useColorModeValue('blue.600', 'blue.400')}
|
||||
color={useColorModeValue('blue.500', 'blue.300')}
|
||||
lineHeight="1.4"
|
||||
cursor="pointer"
|
||||
onClick={(e) => {
|
||||
@@ -290,27 +292,28 @@ const HistoricalEvents = ({
|
||||
|
||||
{/* 时间 + Badges(允许折行) */}
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
<Text fontSize="sm" color={textSecondary}>
|
||||
<Text fontSize="sm" color="gray.300" fontWeight="medium">
|
||||
{formatDate(getEventDate(event))}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={textSecondary}>
|
||||
<Text fontSize="sm" color="gray.400">
|
||||
({getRelativeTime(getEventDate(event))})
|
||||
</Text>
|
||||
{event.importance && (
|
||||
<Badge colorScheme={importanceColor} size="sm">
|
||||
<Badge colorScheme={importanceColor} fontSize="xs" px={2}>
|
||||
重要性: {event.importance}
|
||||
</Badge>
|
||||
)}
|
||||
{event.avg_change_pct !== undefined && event.avg_change_pct !== null && (
|
||||
<Badge
|
||||
colorScheme={event.avg_change_pct > 0 ? 'red' : event.avg_change_pct < 0 ? 'green' : 'gray'}
|
||||
size="sm"
|
||||
fontSize="xs"
|
||||
px={2}
|
||||
>
|
||||
涨幅: {event.avg_change_pct > 0 ? '+' : ''}{event.avg_change_pct.toFixed(2)}%
|
||||
</Badge>
|
||||
)}
|
||||
{event.similarity !== undefined && event.similarity !== null && (
|
||||
<Badge colorScheme={getSimilarityColor(event.similarity)} size="sm">
|
||||
<Badge colorScheme={getSimilarityColor(event.similarity)} fontSize="xs" px={2}>
|
||||
相关度: {event.similarity}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -344,10 +347,9 @@ const HistoricalEvents = ({
|
||||
data={content}
|
||||
title=""
|
||||
showAIBadge={true}
|
||||
textColor={PROFESSIONAL_COLORS.text.primary}
|
||||
textColor="#E2E8F0"
|
||||
containerStyle={{
|
||||
backgroundColor: useColorModeValue('#f7fafc', 'rgba(45, 55, 72, 0.6)'),
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'transparent',
|
||||
padding: '0',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -28,9 +28,11 @@ import {
|
||||
ModalCloseButton,
|
||||
Icon,
|
||||
useColorModeValue,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
Center
|
||||
} from '@chakra-ui/react';
|
||||
import { InfoIcon, ViewIcon } from '@chakra-ui/icons';
|
||||
import { Share2, GitBranch, Inbox } from 'lucide-react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { eventService } from '../../../services/eventService';
|
||||
import CitedContent from '../../../components/Citation/CitedContent';
|
||||
@@ -637,7 +639,7 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box p={6}>
|
||||
<Box>
|
||||
{/* 统计信息条 */}
|
||||
<Box
|
||||
mb={4}
|
||||
@@ -647,56 +649,57 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
||||
borderColor={PROFESSIONAL_COLORS.border.default}
|
||||
bg={PROFESSIONAL_COLORS.background.secondary}
|
||||
>
|
||||
<HStack spacing={6} wrap="wrap">
|
||||
<Stat>
|
||||
<StatLabel color={PROFESSIONAL_COLORS.text.secondary}>总节点数</StatLabel>
|
||||
<StatNumber color={PROFESSIONAL_COLORS.text.primary}>{stats.totalNodes}</StatNumber>
|
||||
<Flex wrap="wrap" gap={{ base: 3, md: 6 }}>
|
||||
<Stat minW="fit-content">
|
||||
<StatLabel fontSize={{ base: "xs", md: "sm" }} color={PROFESSIONAL_COLORS.text.secondary} whiteSpace="nowrap">总节点数</StatLabel>
|
||||
<StatNumber fontSize={{ base: "xl", md: "2xl" }} color={PROFESSIONAL_COLORS.text.primary}>{stats.totalNodes}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel color={PROFESSIONAL_COLORS.text.secondary}>涉及行业</StatLabel>
|
||||
<StatNumber color={PROFESSIONAL_COLORS.text.primary}>{stats.involvedIndustries}</StatNumber>
|
||||
<Stat minW="fit-content">
|
||||
<StatLabel fontSize={{ base: "xs", md: "sm" }} color={PROFESSIONAL_COLORS.text.secondary} whiteSpace="nowrap">涉及行业</StatLabel>
|
||||
<StatNumber fontSize={{ base: "xl", md: "2xl" }} color={PROFESSIONAL_COLORS.text.primary}>{stats.involvedIndustries}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel color={PROFESSIONAL_COLORS.text.secondary}>相关公司</StatLabel>
|
||||
<StatNumber color={PROFESSIONAL_COLORS.text.primary}>{stats.relatedCompanies}</StatNumber>
|
||||
<Stat minW="fit-content">
|
||||
<StatLabel fontSize={{ base: "xs", md: "sm" }} color={PROFESSIONAL_COLORS.text.secondary} whiteSpace="nowrap">相关公司</StatLabel>
|
||||
<StatNumber fontSize={{ base: "xl", md: "2xl" }} color={PROFESSIONAL_COLORS.text.primary}>{stats.relatedCompanies}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel color={PROFESSIONAL_COLORS.text.secondary}>正向影响</StatLabel>
|
||||
<StatNumber color="#10B981">{stats.positiveImpact}</StatNumber>
|
||||
<Stat minW="fit-content">
|
||||
<StatLabel fontSize={{ base: "xs", md: "sm" }} color={PROFESSIONAL_COLORS.text.secondary} whiteSpace="nowrap">正向影响</StatLabel>
|
||||
<StatNumber fontSize={{ base: "xl", md: "2xl" }} color="#10B981">{stats.positiveImpact}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel color={PROFESSIONAL_COLORS.text.secondary}>负向影响</StatLabel>
|
||||
<StatNumber color="#EF4444">{stats.negativeImpact}</StatNumber>
|
||||
<Stat minW="fit-content">
|
||||
<StatLabel fontSize={{ base: "xs", md: "sm" }} color={PROFESSIONAL_COLORS.text.secondary} whiteSpace="nowrap">负向影响</StatLabel>
|
||||
<StatNumber fontSize={{ base: "xl", md: "2xl" }} color="#EF4444">{stats.negativeImpact}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel color={PROFESSIONAL_COLORS.text.secondary}>循环效应</StatLabel>
|
||||
<StatNumber color="#A855F7">{stats.circularEffect}</StatNumber>
|
||||
<Stat minW="fit-content">
|
||||
<StatLabel fontSize={{ base: "xs", md: "sm" }} color={PROFESSIONAL_COLORS.text.secondary} whiteSpace="nowrap">循环效应</StatLabel>
|
||||
<StatNumber fontSize={{ base: "xl", md: "2xl" }} color="#A855F7">{stats.circularEffect}</StatNumber>
|
||||
</Stat>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 自定义图例 */}
|
||||
<Box mb={4}>
|
||||
<HStack spacing={4} wrap="wrap">
|
||||
{Object.entries(NODE_STYLES).map(([type, style]) => (
|
||||
<Tag
|
||||
key={type}
|
||||
size="md"
|
||||
bg={PROFESSIONAL_COLORS.background.secondary}
|
||||
color={PROFESSIONAL_COLORS.text.primary}
|
||||
borderWidth="1px"
|
||||
borderColor={PROFESSIONAL_COLORS.border.default}
|
||||
>
|
||||
<Box w={3} h={3} bg={style.color} borderRadius="sm" mr={2} />
|
||||
{NODE_TYPE_LABELS[type] || type}
|
||||
</Tag>
|
||||
))}
|
||||
</HStack>
|
||||
</Box>
|
||||
<Flex mb={4} wrap="wrap" gap={2}>
|
||||
{Object.entries(NODE_STYLES).map(([type, style]) => (
|
||||
<Tag
|
||||
key={type}
|
||||
size="sm"
|
||||
px={2}
|
||||
py={1}
|
||||
bg={PROFESSIONAL_COLORS.background.secondary}
|
||||
color={PROFESSIONAL_COLORS.text.primary}
|
||||
borderWidth="1px"
|
||||
borderColor={PROFESSIONAL_COLORS.border.default}
|
||||
>
|
||||
<Box w={2.5} h={2.5} bg={style.color} borderRadius="sm" mr={1.5} />
|
||||
{NODE_TYPE_LABELS[type] || type}
|
||||
</Tag>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
{/* 视图切换按钮 */}
|
||||
<Flex mb={4} gap={2}>
|
||||
<Button
|
||||
leftIcon={<Icon as={Share2} boxSize={4} />}
|
||||
bg={viewMode === 'graph' ? PROFESSIONAL_COLORS.gold[500] : PROFESSIONAL_COLORS.background.secondary}
|
||||
color={viewMode === 'graph' ? 'black' : PROFESSIONAL_COLORS.text.primary}
|
||||
_hover={{
|
||||
@@ -710,6 +713,7 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
||||
力导向图
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<Icon as={GitBranch} boxSize={4} />}
|
||||
bg={viewMode === 'sankey' ? PROFESSIONAL_COLORS.gold[500] : PROFESSIONAL_COLORS.background.secondary}
|
||||
color={viewMode === 'sankey' ? 'black' : PROFESSIONAL_COLORS.text.primary}
|
||||
_hover={{
|
||||
@@ -722,7 +726,6 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
||||
>
|
||||
桑基图
|
||||
</Button>
|
||||
|
||||
</Flex>
|
||||
|
||||
{loading && (
|
||||
@@ -748,75 +751,97 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
||||
|
||||
{!loading && !error && (
|
||||
<Box>
|
||||
{/* 提示信息 */}
|
||||
<Alert
|
||||
status="info"
|
||||
mb={4}
|
||||
borderRadius="md"
|
||||
bg="rgba(59, 130, 246, 0.1)"
|
||||
color="#3B82F6"
|
||||
borderWidth="1px"
|
||||
borderColor="#3B82F6"
|
||||
>
|
||||
<AlertIcon />
|
||||
<Text fontSize="sm" color={PROFESSIONAL_COLORS.text.secondary}>
|
||||
<Icon as={ViewIcon} mr={2} />
|
||||
点击图表中的节点可以查看详细信息
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
{/* 图表容器 */}
|
||||
{/* 图表容器 - 宽高比 2:1,H5 自适应 */}
|
||||
<Box
|
||||
h={viewMode === 'sankey' ? "600px" : "700px"}
|
||||
position="relative"
|
||||
w="100%"
|
||||
pb={{ base: "75%", md: "50%" }}
|
||||
border="1px solid"
|
||||
borderColor={PROFESSIONAL_COLORS.border.default}
|
||||
borderRadius="lg"
|
||||
boxShadow="0 4px 12px rgba(0, 0, 0, 0.3)"
|
||||
bg={PROFESSIONAL_COLORS.background.card}
|
||||
p={4}
|
||||
ref={containerRef}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
p={4}
|
||||
>
|
||||
{/* 提示信息 - 固定在左上角 */}
|
||||
<Text
|
||||
position="absolute"
|
||||
top={2}
|
||||
left={3}
|
||||
fontSize="xs"
|
||||
color={PROFESSIONAL_COLORS.text.muted}
|
||||
zIndex={1}
|
||||
bg="rgba(0, 0, 0, 0.5)"
|
||||
px={2}
|
||||
py={1}
|
||||
borderRadius="md"
|
||||
>
|
||||
<Icon as={ViewIcon} mr={1} boxSize={3} />
|
||||
点击节点查看详情
|
||||
</Text>
|
||||
{chartReady && (
|
||||
<>
|
||||
{viewMode === 'graph' ? (
|
||||
<ReactECharts
|
||||
option={graphData ? getGraphOption(graphData) : {}}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onEvents={{
|
||||
click: handleGraphNodeClick
|
||||
}}
|
||||
opts={{
|
||||
renderer: 'canvas',
|
||||
devicePixelRatio: window.devicePixelRatio || 1
|
||||
}}
|
||||
lazyUpdate={true}
|
||||
notMerge={false}
|
||||
shouldSetOption={(prevProps, props) => {
|
||||
// 减少不必要的重新渲染
|
||||
return JSON.stringify(prevProps.option) !== JSON.stringify(props.option);
|
||||
}}
|
||||
/>
|
||||
{/* 空状态提示 */}
|
||||
{(viewMode === 'graph' && (!graphData || !graphData.nodes || graphData.nodes.length === 0)) ||
|
||||
(viewMode === 'sankey' && (!sankeyData || !sankeyData.nodes || sankeyData.nodes.length === 0)) ? (
|
||||
<Center h="100%" flexDirection="column">
|
||||
<Icon as={Inbox} boxSize={12} color={PROFESSIONAL_COLORS.text.muted} />
|
||||
<Text mt={4} color={PROFESSIONAL_COLORS.text.muted} fontSize="sm">
|
||||
暂无传导链数据
|
||||
</Text>
|
||||
</Center>
|
||||
) : (
|
||||
<ReactECharts
|
||||
option={sankeyData ? getSankeyOption(sankeyData) : {}}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onEvents={{
|
||||
click: handleSankeyNodeClick
|
||||
}}
|
||||
opts={{
|
||||
renderer: 'canvas',
|
||||
devicePixelRatio: window.devicePixelRatio || 1
|
||||
}}
|
||||
lazyUpdate={true}
|
||||
notMerge={false}
|
||||
shouldSetOption={(prevProps, props) => {
|
||||
// 减少不必要的重新渲染
|
||||
return JSON.stringify(prevProps.option) !== JSON.stringify(props.option);
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
{viewMode === 'graph' ? (
|
||||
<ReactECharts
|
||||
option={graphData ? getGraphOption(graphData) : {}}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onEvents={{
|
||||
click: handleGraphNodeClick
|
||||
}}
|
||||
opts={{
|
||||
renderer: 'canvas',
|
||||
devicePixelRatio: window.devicePixelRatio || 1
|
||||
}}
|
||||
lazyUpdate={true}
|
||||
notMerge={false}
|
||||
shouldSetOption={(prevProps, props) => {
|
||||
// 减少不必要的重新渲染
|
||||
return JSON.stringify(prevProps.option) !== JSON.stringify(props.option);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ReactECharts
|
||||
option={sankeyData ? getSankeyOption(sankeyData) : {}}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onEvents={{
|
||||
click: handleSankeyNodeClick
|
||||
}}
|
||||
opts={{
|
||||
renderer: 'canvas',
|
||||
devicePixelRatio: window.devicePixelRatio || 1
|
||||
}}
|
||||
lazyUpdate={true}
|
||||
notMerge={false}
|
||||
shouldSetOption={(prevProps, props) => {
|
||||
// 减少不必要的重新渲染
|
||||
return JSON.stringify(prevProps.option) !== JSON.stringify(props.option);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
@@ -826,8 +851,8 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
||||
<Modal isOpen={isModalOpen} onClose={handleCloseModal} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxH="80vh" bg={modalBgColor}>
|
||||
<ModalHeader borderBottom="1px solid" borderColor={modalBorderColor}>
|
||||
<HStack justify="space-between">
|
||||
<ModalHeader borderBottom="1px solid" borderColor={modalBorderColor} pr={12}>
|
||||
<HStack justify="space-between" pr={2}>
|
||||
<Text color={PROFESSIONAL_COLORS.text.primary}>{selectedNode ? '节点详情' : '传导链分析'}</Text>
|
||||
{selectedNode && (
|
||||
<Badge
|
||||
@@ -841,7 +866,10 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
||||
)}
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalCloseButton
|
||||
color={PROFESSIONAL_COLORS.text.secondary}
|
||||
_hover={{ bg: 'rgba(255, 255, 255, 0.1)' }}
|
||||
/>
|
||||
|
||||
<ModalBody overflowY="auto">
|
||||
{selectedNode ? (
|
||||
@@ -1084,11 +1112,15 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
||||
prefix="机制:"
|
||||
prefixStyle={{ fontSize: 12, color: PROFESSIONAL_COLORS.text.secondary, fontWeight: 'bold' }}
|
||||
textColor={PROFESSIONAL_COLORS.text.primary}
|
||||
containerStyle={{ marginTop: 8 }}
|
||||
containerStyle={{
|
||||
marginTop: 8,
|
||||
backgroundColor: 'transparent',
|
||||
padding: 0,
|
||||
}}
|
||||
showAIBadge={false}
|
||||
/>
|
||||
) : parent.transmission_mechanism ? (
|
||||
<Text fontSize="xs" color="gray.600">
|
||||
<Text fontSize="xs" color={PROFESSIONAL_COLORS.text.secondary}>
|
||||
机制: {parent.transmission_mechanism}(AI合成)
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -1109,19 +1141,38 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
||||
if (targetsFromAPI) {
|
||||
return (
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={2} color="blue.600">
|
||||
影响输出 ({nodeDetail.children.length})
|
||||
<Text fontWeight="bold" mb={2} color={PROFESSIONAL_COLORS.gold[500]}>
|
||||
影响输出 ({nodeDetail.children.length})(AI合成)
|
||||
</Text>
|
||||
<List spacing={2}>
|
||||
{nodeDetail.children.map((child, index) => (
|
||||
<ListItem key={index} p={2} bg="gray.50" borderRadius="md" borderLeft="3px solid" borderColor="orange.300" position="relative">
|
||||
<ListItem
|
||||
key={index}
|
||||
p={2}
|
||||
bg={PROFESSIONAL_COLORS.background.secondary}
|
||||
borderRadius="md"
|
||||
borderLeft="3px solid"
|
||||
borderColor="#FB923C"
|
||||
position="relative"
|
||||
>
|
||||
{child.direction && (
|
||||
<Box position="absolute" top={2} right={2} zIndex={1}>
|
||||
<Badge
|
||||
colorScheme={
|
||||
child.direction === 'positive' ? 'green' :
|
||||
child.direction === 'negative' ? 'red' :
|
||||
'gray'
|
||||
bg={
|
||||
child.direction === 'positive' ? 'rgba(16, 185, 129, 0.15)' :
|
||||
child.direction === 'negative' ? 'rgba(239, 68, 68, 0.15)' :
|
||||
'rgba(107, 114, 128, 0.15)'
|
||||
}
|
||||
color={
|
||||
child.direction === 'positive' ? '#10B981' :
|
||||
child.direction === 'negative' ? '#EF4444' :
|
||||
'#6B7280'
|
||||
}
|
||||
borderWidth="1px"
|
||||
borderColor={
|
||||
child.direction === 'positive' ? '#10B981' :
|
||||
child.direction === 'negative' ? '#EF4444' :
|
||||
'#6B7280'
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
@@ -1132,7 +1183,7 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
||||
</Box>
|
||||
)}
|
||||
<VStack align="stretch" spacing={1}>
|
||||
<Text fontWeight="bold" fontSize="sm" pr={child.direction ? 20 : 0}>{child.name}</Text>
|
||||
<Text fontWeight="bold" fontSize="sm" color={PROFESSIONAL_COLORS.text.primary} pr={child.direction ? 20 : 0}>{child.name}</Text>
|
||||
{child.transmission_mechanism?.data ? (
|
||||
<CitedContent
|
||||
data={child.transmission_mechanism}
|
||||
@@ -1140,11 +1191,15 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
||||
prefix="机制:"
|
||||
prefixStyle={{ fontSize: 12, color: PROFESSIONAL_COLORS.text.secondary, fontWeight: 'bold' }}
|
||||
textColor={PROFESSIONAL_COLORS.text.primary}
|
||||
containerStyle={{ marginTop: 8 }}
|
||||
containerStyle={{
|
||||
marginTop: 8,
|
||||
backgroundColor: 'transparent',
|
||||
padding: 0,
|
||||
}}
|
||||
showAIBadge={false}
|
||||
/>
|
||||
) : child.transmission_mechanism ? (
|
||||
<Text fontSize="xs" color="gray.600">
|
||||
<Text fontSize="xs" color={PROFESSIONAL_COLORS.text.secondary}>
|
||||
机制: {child.transmission_mechanism}(AI合成)
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -1169,7 +1224,14 @@ const TransmissionChainAnalysis = ({ eventId }) => {
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter borderTop="1px solid" borderColor={modalBorderColor}>
|
||||
<Button onClick={handleCloseModal}>关闭</Button>
|
||||
<Button
|
||||
onClick={handleCloseModal}
|
||||
variant="ghost"
|
||||
color={PROFESSIONAL_COLORS.text.secondary}
|
||||
_hover={{ bg: 'rgba(255, 255, 255, 0.1)' }}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user