community增加事件详情
This commit is contained in:
213
app.py
213
app.py
@@ -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():
|
||||
"""获取指定日期的事件列表"""
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user