community增加事件详情

This commit is contained in:
2026-01-07 16:33:30 +08:00
parent 9b42c2c7c2
commit 131e92b0b9
3 changed files with 324 additions and 149 deletions

213
app.py
View File

@@ -10541,6 +10541,219 @@ def get_event_counts():
}), 500
@app.route('/api/v1/calendar/combined-data', methods=['GET'])
def get_calendar_combined_data():
"""获取日历综合数据(涨停数据 + 事件数量 + 上证涨跌幅)
一次性返回日历显示所需的所有数据,避免前端多次请求
参数:
year: 年份(可选,默认当前年份)
month: 月份(可选,默认当前月份)
返回:
{
success: true,
data: [
{
date: "20260107",
zt_count: 80, // 涨停数量
top_sector: "人工智能", // 最热概念
event_count: 8, // 事件数量
index_change: 1.38 // 上证涨跌幅(%)
},
...
]
}
"""
import os
import json
try:
year = request.args.get('year', datetime.now().year, type=int)
month = request.args.get('month', datetime.now().month, type=int)
# 计算月份的开始和结束日期
start_date = datetime(year, month, 1)
if month == 12:
end_date = datetime(year + 1, 1, 1)
else:
end_date = datetime(year, month + 1, 1)
# 用 Map 存储数据key 为 YYYYMMDD 格式
data_map = {}
# 1. 从静态文件加载涨停数据
zt_dates_path = os.path.join(app.static_folder or 'static', 'data', 'zt', 'dates.json')
# 尝试多个可能的路径
possible_paths = [
zt_dates_path,
os.path.join('public', 'data', 'zt', 'dates.json'),
os.path.join('data', 'zt', 'dates.json'),
'/var/www/vf_react/data/zt/dates.json', # 服务器路径
]
zt_dates = []
for path in possible_paths:
if os.path.exists(path):
try:
with open(path, 'r', encoding='utf-8') as f:
zt_data = json.load(f)
zt_dates = zt_data.get('dates', [])
break
except Exception as e:
print(f"[calendar] 读取涨停数据失败 {path}: {e}")
# 处理涨停数据
for item in zt_dates:
date_str = item.get('date', '')
if not date_str:
continue
# 检查是否在当月范围内
try:
item_date = datetime.strptime(date_str, '%Y%m%d')
if start_date <= item_date < end_date:
data_map[date_str] = {
'date': date_str,
'zt_count': item.get('count', 0),
'top_sector': '',
'event_count': 0,
'index_change': None
}
except ValueError:
continue
# 2. 加载涨停详情获取 top_sector从 daily 文件)
zt_daily_dir = None
possible_daily_dirs = [
os.path.join(app.static_folder or 'static', 'data', 'zt', 'daily'),
os.path.join('public', 'data', 'zt', 'daily'),
os.path.join('data', 'zt', 'daily'),
'/var/www/vf_react/data/zt/daily',
]
for dir_path in possible_daily_dirs:
if os.path.isdir(dir_path):
zt_daily_dir = dir_path
break
if zt_daily_dir:
for date_str in data_map.keys():
daily_file = os.path.join(zt_daily_dir, f'{date_str}.json')
if os.path.exists(daily_file):
try:
with open(daily_file, 'r', encoding='utf-8') as f:
daily_data = json.load(f)
# 优先使用词云图最高频词
word_freq = daily_data.get('word_freq_data', [])
if word_freq:
data_map[date_str]['top_sector'] = word_freq[0].get('name', '')
elif daily_data.get('sector_data'):
# 找出涨停数最多的板块
max_sector = ''
max_count = 0
for sector, info in daily_data['sector_data'].items():
if info.get('count', 0) > max_count:
max_count = info['count']
max_sector = sector
data_map[date_str]['top_sector'] = max_sector
except Exception as e:
print(f"[calendar] 读取涨停详情失败 {daily_file}: {e}")
# 3. 查询事件数量
event_query = """
SELECT DATE(calendar_time) as date, COUNT(*) as count
FROM future_events
WHERE calendar_time BETWEEN :start_date AND :end_date
AND type = 'event'
GROUP BY DATE(calendar_time)
"""
event_result = db.session.execute(text(event_query), {
'start_date': start_date,
'end_date': end_date
})
for row in event_result:
event_date = row[0]
event_count = row[1]
date_str = event_date.strftime('%Y%m%d') if hasattr(event_date, 'strftime') else str(event_date).replace('-', '')
if date_str in data_map:
data_map[date_str]['event_count'] = event_count
else:
data_map[date_str] = {
'date': date_str,
'zt_count': 0,
'top_sector': '',
'event_count': event_count,
'index_change': None
}
# 4. 查询上证指数涨跌幅(从 MySQL 的 ea_exchangetrade 表)
index_query = """
SELECT TRADEDATE, F006N, F007N
FROM ea_exchangetrade
WHERE INDEXCODE = '000001'
AND TRADEDATE BETWEEN :start_date AND :end_date
ORDER BY TRADEDATE
"""
try:
with engine.connect() as conn:
index_result = conn.execute(text(index_query), {
'start_date': start_date,
'end_date': end_date
}).fetchall()
# 构建前一天收盘价映射,用于计算涨跌幅
prev_close = None
for row in index_result:
trade_date = row[0]
close_price = float(row[1]) if row[1] else None
db_prev_close = float(row[2]) if row[2] else None
date_str = trade_date.strftime('%Y%m%d') if hasattr(trade_date, 'strftime') else str(trade_date).replace('-', '')
# 计算涨跌幅
actual_prev = prev_close if prev_close else db_prev_close
if close_price and actual_prev:
change_pct = ((close_price - actual_prev) / actual_prev) * 100
if date_str in data_map:
data_map[date_str]['index_change'] = round(change_pct, 2)
else:
data_map[date_str] = {
'date': date_str,
'zt_count': 0,
'top_sector': '',
'event_count': 0,
'index_change': round(change_pct, 2)
}
prev_close = close_price
except Exception as e:
print(f"[calendar] 查询上证指数失败: {e}")
# 按日期排序返回
result = sorted(data_map.values(), key=lambda x: x['date'])
return jsonify({
'success': True,
'data': result,
'year': year,
'month': month
})
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/v1/calendar/events', methods=['GET'])
def get_calendar_events():
"""获取指定日期的事件列表"""

View File

@@ -1788,4 +1788,96 @@ export const eventHandlers = [
);
}
}),
// ==================== 日历综合数据(一次性获取所有数据)====================
// 获取日历综合数据(涨停 + 事件 + 上证涨跌幅)
http.get('/api/v1/calendar/combined-data', async ({ request }) => {
await delay(400);
const url = new URL(request.url);
const year = parseInt(url.searchParams.get('year')) || new Date().getFullYear();
const month = parseInt(url.searchParams.get('month')) || new Date().getMonth() + 1;
console.log('[Mock] 获取日历综合数据:', { year, month });
try {
const data = [];
const today = new Date();
const daysInMonth = new Date(year, month, 0).getDate();
// 热门概念列表
const hotConcepts = [
'人工智能', '华为鸿蒙', '机器人', '芯片', '算力', '新能源',
'固态电池', '量子计算', '低空经济', '智能驾驶', '光伏', '储能'
];
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month - 1, day);
const dayOfWeek = date.getDay();
// 跳过周末
if (dayOfWeek === 0 || dayOfWeek === 6) continue;
const dateStr = `${year}${String(month).padStart(2, '0')}${String(day).padStart(2, '0')}`;
const isPast = date < today;
const isToday = date.toDateString() === today.toDateString();
const isFuture = date > today;
// 使用日期作为种子生成一致的随机数
const dateSeed = year * 10000 + month * 100 + day;
const seededRandom = (seed) => {
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
};
const item = {
date: dateStr,
zt_count: 0,
top_sector: '',
event_count: 0,
index_change: null
};
// 历史数据:涨停 + 上证涨跌幅
if (isPast || isToday) {
item.zt_count = Math.floor(seededRandom(dateSeed) * 80 + 30); // 30-110
item.top_sector = hotConcepts[Math.floor(seededRandom(dateSeed + 1) * hotConcepts.length)];
item.index_change = parseFloat((seededRandom(dateSeed + 2) * 4 - 2).toFixed(2)); // -2% ~ +2%
}
// 未来数据:事件数量
if (isFuture) {
const hasEvents = seededRandom(dateSeed + 3) > 0.4; // 60% 概率有事件
if (hasEvents) {
item.event_count = Math.floor(seededRandom(dateSeed + 4) * 10 + 1); // 1-10
}
}
// 今天:同时有涨停和事件
if (isToday) {
item.event_count = Math.floor(seededRandom(dateSeed + 5) * 8 + 2); // 2-10
}
data.push(item);
}
return HttpResponse.json({
success: true,
data: data,
year: year,
month: month
});
} catch (error) {
console.error('[Mock] 获取日历综合数据失败:', error);
return HttpResponse.json(
{
success: false,
error: '获取日历综合数据失败',
data: []
},
{ status: 500 }
);
}
}),
];

View File

@@ -2316,174 +2316,44 @@ const CombinedCalendar = () => {
const [currentMonth, setCurrentMonth] = useState(new Date());
const [selectedDate, setSelectedDate] = useState(null);
// 涨停数据
const [ztDatesData, setZtDatesData] = useState([]);
// 日历综合数据(涨停 + 事件 + 上证涨跌幅)- 使用新的综合 API
const [calendarData, setCalendarData] = useState([]);
const [ztDailyDetails, setZtDailyDetails] = useState({});
const [selectedZtDetail, setSelectedZtDetail] = useState(null);
// 投资日历数据
const [eventCounts, setEventCounts] = useState([]);
const [selectedEvents, setSelectedEvents] = useState([]);
// 上证指数涨跌幅数据
const [indexChangeMap, setIndexChangeMap] = useState({});
const [detailLoading, setDetailLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
// 加载涨停 dates.json
// 加载日历综合数据(一次 API 调用获取所有数据)
useEffect(() => {
const loadZtDatesData = async () => {
try {
const response = await fetch('/data/zt/dates.json');
if (response.ok) {
const data = await response.json();
setZtDatesData(data.dates || []);
}
} catch (error) {
console.error('Failed to load zt dates.json:', error);
}
};
loadZtDatesData();
}, []);
// 加载投资日历事件数量
useEffect(() => {
const loadEventCounts = async () => {
const loadCalendarCombinedData = async () => {
try {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth() + 1;
const response = await eventService.calendar.getEventCounts(year, month);
if (response.success) {
setEventCounts(response.data || []);
}
} catch (error) {
console.error('Failed to load event counts:', error);
}
};
loadEventCounts();
}, [currentMonth]);
// 加载上证指数历史涨跌幅数据
useEffect(() => {
const loadIndexData = async () => {
try {
const response = await fetch(`${getApiBase()}/api/index/000001.SH/kline?type=daily`);
const response = await fetch(`${getApiBase()}/api/v1/calendar/combined-data?year=${year}&month=${month}`);
if (response.ok) {
const result = await response.json();
// API 直接返回 { code, name, data, ... },没有 success 字段
if (result.data && Array.isArray(result.data)) {
// 构建日期到涨跌幅的映射
const changeMap = {};
result.data.forEach(item => {
// API返回的是 time 字段(不是 date格式是 YYYY-MM-DD转为 YYYYMMDD
const dateField = item.time || item.date;
if (!dateField) return;
const yyyymmdd = dateField.replace(/-/g, '');
// 计算涨跌幅 = (close - prev_close) / prev_close * 100
if (item.close && item.prev_close) {
const change = ((item.close - item.prev_close) / item.prev_close) * 100;
changeMap[yyyymmdd] = change;
}
});
console.log('[HeroPanel] 加载上证指数数据成功,数据条数:', result.data.length, '映射条目数:', Object.keys(changeMap).length);
setIndexChangeMap(changeMap);
if (result.success && result.data) {
// 转换为 FullCalendarPro 需要的格式
const formattedData = result.data.map(item => ({
date: item.date,
count: item.zt_count || 0,
topSector: item.top_sector || '',
eventCount: item.event_count || 0,
indexChange: item.index_change,
}));
console.log('[HeroPanel] 加载日历综合数据成功,数据条数:', formattedData.length);
setCalendarData(formattedData);
}
}
} catch (error) {
console.error('Failed to load index data:', error);
console.error('Failed to load calendar combined data:', error);
}
};
loadIndexData();
}, []);
loadCalendarCombinedData();
}, [currentMonth]);
// 获取涨停板块详情(加载所有数据,不限于当月)
useEffect(() => {
const loadZtDetails = async () => {
const details = {};
const promises = [];
// 加载所有有数据的日期
ztDatesData.forEach(d => {
const dateStr = d.date;
if (!ztDailyDetails[dateStr]) {
promises.push(
fetch(`/data/zt/daily/${dateStr}.json`)
.then(res => res.ok ? res.json() : null)
.then(data => {
if (data) {
// 优先使用词云图最高频词fallback 到板块数据
let topWord = '';
if (data.word_freq_data && data.word_freq_data.length > 0) {
topWord = data.word_freq_data[0].name;
} else if (data.sector_data) {
let maxCount = 0;
Object.entries(data.sector_data).forEach(([sector, info]) => {
if (info.count > maxCount) {
maxCount = info.count;
topWord = sector;
}
});
}
details[dateStr] = { top_sector: topWord, fullData: data };
}
})
.catch(() => null)
);
}
});
if (promises.length > 0) {
await Promise.all(promises);
setZtDailyDetails(prev => ({ ...prev, ...details }));
}
};
if (ztDatesData.length > 0) {
loadZtDetails();
}
}, [ztDatesData]);
// 构建 FullCalendarPro 所需的数据格式
// 需要合并涨停数据、未来事件数据和上证指数涨跌幅
const calendarData = useMemo(() => {
// 创建日期到数据的映射
const dataMap = new Map();
// 先添加涨停数据
ztDatesData.forEach(d => {
const detail = ztDailyDetails[d.date] || {};
dataMap.set(d.date, {
date: d.date,
count: d.count,
topSector: detail.top_sector || '',
eventCount: 0,
indexChange: indexChangeMap[d.date] ?? null,
});
});
// 再添加/合并未来事件数据
eventCounts.forEach(e => {
// e.date 格式是 YYYY-MM-DD需要转为 YYYYMMDD
const yyyymmdd = e.date.replace(/-/g, '');
if (dataMap.has(yyyymmdd)) {
// 已有涨停数据,只更新事件数
const existing = dataMap.get(yyyymmdd);
existing.eventCount = e.count;
} else {
// 纯未来事件日期,没有涨停数据
dataMap.set(yyyymmdd, {
date: yyyymmdd,
count: 0,
topSector: '',
eventCount: e.count,
indexChange: indexChangeMap[yyyymmdd] ?? null,
});
}
});
return Array.from(dataMap.values());
}, [ztDatesData, ztDailyDetails, eventCounts, indexChangeMap]);
// 处理日期点击 - 打开弹窗
const handleDateClick = useCallback(async (date) => {