diff --git a/MeAgent/src/screens/Market/EventCalendar.js b/MeAgent/src/screens/Market/EventCalendar.js index 86a76a65..14e201a9 100644 --- a/MeAgent/src/screens/Market/EventCalendar.js +++ b/MeAgent/src/screens/Market/EventCalendar.js @@ -31,7 +31,7 @@ import ztService from '../../services/ztService'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const CELL_WIDTH = (SCREEN_WIDTH - 32) / 7; -const CELL_HEIGHT = 90; // 增加高度以容纳更多内容 +const CELL_HEIGHT = 95; // 增加高度以容纳跨天概念条 // 概念颜色调色板 const CONCEPT_COLORS = [ @@ -72,26 +72,32 @@ const formatDateStr = (year, month, day) => { return `${year}${m}${d}`; }; -// 检查是否连续日期(跳过周末) -const isConsecutiveDate = (date1, date2) => { - const d1 = new Date( - parseInt(date1.slice(0, 4)), - parseInt(date1.slice(4, 6)) - 1, - parseInt(date1.slice(6, 8)) - ); - const d2 = new Date( - parseInt(date2.slice(0, 4)), - parseInt(date2.slice(4, 6)) - 1, - parseInt(date2.slice(6, 8)) +// 解析日期字符串为 Date 对象 +const parseDateStr = (dateStr) => { + return new Date( + parseInt(dateStr.slice(0, 4)), + parseInt(dateStr.slice(4, 6)) - 1, + parseInt(dateStr.slice(6, 8)) ); +}; + +// 检查是否是下一个交易日(跳过周末) +const isNextTradingDay = (date1, date2) => { + const d1 = parseDateStr(date1); + const d2 = parseDateStr(date2); const diff = (d2 - d1) / (1000 * 60 * 60 * 24); + + // 直接相邻 if (diff === 1) return true; - if (diff === 2 && d1.getDay() === 5) return true; // 周五到周日 - if (diff === 3 && d1.getDay() === 5) return true; // 周五到周一 + // 周五到周一(跳过周末) + if (diff === 3 && d1.getDay() === 5) return true; + // 周六到周一 + if (diff === 2 && d1.getDay() === 6) return true; + return false; }; -// 合并连续相同概念 +// 合并连续相同概念(跨周显示由分段逻辑处理) const mergeConsecutiveConcepts = (calendarData, year, month) => { const sorted = [...calendarData] .filter(d => d.topSector) @@ -102,17 +108,22 @@ const mergeConsecutiveConcepts = (calendarData, year, month) => { sorted.forEach((item, index) => { const prevItem = sorted[index - 1]; - const isConsecutive = prevItem && - item.topSector === prevItem.topSector && - isConsecutiveDate(prevItem.date, item.date); - if (isConsecutive && currentEvent) { + // 检查是否应该继续当前事件 + const shouldContinue = prevItem && + currentEvent && + item.topSector === prevItem.topSector && + isNextTradingDay(prevItem.date, item.date); + + if (shouldContinue) { currentEvent.endDate = item.date; currentEvent.dates.push(item.date); } else { + // 保存之前的事件(如果有多天) if (currentEvent && currentEvent.dates.length > 1) { events.push(currentEvent); } + // 开始新事件 currentEvent = { concept: item.topSector, startDate: item.date, @@ -122,6 +133,7 @@ const mergeConsecutiveConcepts = (calendarData, year, month) => { } }); + // 保存最后一个事件 if (currentEvent && currentEvent.dates.length > 1) { events.push(currentEvent); } @@ -129,6 +141,56 @@ const mergeConsecutiveConcepts = (calendarData, year, month) => { return events; }; +// 将概念事件分割成多行显示的段(处理跨周情况) +// 基于实际的 dates 数组而不是日期范围 +const splitEventIntoSegments = (event, startingDay, daysInMonth) => { + const segments = []; + const { dates, concept } = event; + + if (!dates || dates.length === 0) return segments; + + let currentSegment = null; + + dates.forEach((dateStr, idx) => { + const day = parseInt(dateStr.slice(6, 8)); + const col = (startingDay + day - 1) % 7; + const row = Math.floor((startingDay + day - 1) / 7); + + // 检查是否应该开始新段 + // 条件:没有当前段,或者行变化了 + if (!currentSegment || currentSegment.row !== row) { + // 保存之前的段 + if (currentSegment) { + segments.push(currentSegment); + } + // 开始新段 + currentSegment = { + startDay: day, + endDay: day, + startCol: col, + endCol: col, + row: row, + totalDays: dates.length, + concept: concept, + isStart: idx === 0, + isEnd: idx === dates.length - 1, + }; + } else { + // 扩展当前段 + currentSegment.endDay = day; + currentSegment.endCol = col; + currentSegment.isEnd = idx === dates.length - 1; + } + }); + + // 保存最后一个段 + if (currentSegment) { + segments.push(currentSegment); + } + + return segments; +}; + // 火焰图标组件 const FlameIcon = ({ color, size = 14 }) => ( @@ -222,27 +284,39 @@ const EventCalendar = ({ navigation }) => { navigation.navigate('MarketHot', { date: dateStr }); }, [currentYear, currentMonth, navigation]); - // 计算概念条位置 - const getConceptBarPosition = (event, rowIndex) => { - const startDay = parseInt(event.startDate.slice(6, 8)); - const endDay = parseInt(event.endDate.slice(6, 8)); + // 获取所有概念条的显示段 + const conceptSegments = useMemo(() => { + const allSegments = []; - // 计算在网格中的位置 - const startCol = (startingDay + startDay - 1) % 7; - const startRow = Math.floor((startingDay + startDay - 1) / 7); - const endCol = (startingDay + endDay - 1) % 7; - const endRow = Math.floor((startingDay + endDay - 1) / 7); + conceptEvents.forEach((event, eventIndex) => { + const segments = splitEventIntoSegments(event, startingDay, daysInMonth); + segments.forEach((segment, segIndex) => { + allSegments.push({ + ...segment, + eventIndex, + segmentIndex: segIndex, + color: getConceptColor(event.concept), + }); + }); + }); - // 只处理同一行的情况(简化版) - if (startRow !== endRow) return null; - if (startRow !== rowIndex) return null; + // 按行分组,用于计算同一行内的堆叠 + const byRow = {}; + allSegments.forEach(seg => { + if (!byRow[seg.row]) byRow[seg.row] = []; + byRow[seg.row].push(seg); + }); - return { - left: startCol * CELL_WIDTH + 4, - width: (endCol - startCol + 1) * CELL_WIDTH - 8, - row: startRow, - }; - }; + // 为每个段分配垂直偏移(同一行内多个概念条时) + Object.values(byRow).forEach(rowSegments => { + rowSegments.forEach((seg, idx) => { + seg.stackIndex = idx; + seg.stackTotal = rowSegments.length; + }); + }); + + return allSegments; + }, [conceptEvents, startingDay, daysInMonth]); // 渲染日历格子 const renderCalendarCells = () => { @@ -373,52 +447,58 @@ const EventCalendar = ({ navigation }) => { // 渲染概念横条 const renderConceptBars = () => { - const totalRows = Math.ceil((startingDay + daysInMonth) / 7); - const bars = []; + return conceptSegments.map((segment, index) => { + const { startCol, endCol, row, totalDays, concept, color, isStart, stackIndex } = segment; - conceptEvents.forEach((event, eventIndex) => { - for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) { - const position = getConceptBarPosition(event, rowIndex); - if (!position) continue; + // 计算位置和尺寸 + const left = startCol * CELL_WIDTH + 2; + const width = (endCol - startCol + 1) * CELL_WIDTH - 4; + // 根据堆叠索引调整垂直位置(每个概念条高度 20,间隔 2) + const barHeight = 20; + const verticalOffset = stackIndex * (barHeight + 2); + const top = row * CELL_HEIGHT + CELL_HEIGHT - 26 - verticalOffset; - const color = getConceptColor(event.concept); - const daysCount = event.dates.length; - - bars.push( - + - - - {event.concept} - {daysCount > 1 && ( - - {' '}({daysCount}天) - - )} - - - - ); - } + {concept} + {totalDays > 1 && isStart && ( + + ({totalDays}天) + + )} + + + + ); }); - - return bars; }; return ( @@ -641,11 +721,10 @@ const styles = StyleSheet.create({ borderRadius: 16, }, conceptBar: { - height: 22, - borderRadius: 6, + height: 20, alignItems: 'center', justifyContent: 'center', - paddingHorizontal: 8, + paddingHorizontal: 6, }, legendBar: { width: 20, diff --git a/MeAgent/src/services/ztService.js b/MeAgent/src/services/ztService.js index d796bfbb..cfb4eb0f 100644 --- a/MeAgent/src/services/ztService.js +++ b/MeAgent/src/services/ztService.js @@ -266,34 +266,29 @@ export const ztService = { }, /** - * 快速获取日历数据(只从 dates.json 获取基础信息) + * 快速获取日历数据(从 API 获取完整信息包括 top_sector) * @param {number} year - 年份 * @param {number} month - 月份 (1-12) * @returns {Promise} 日历数据 */ getCalendarDataFast: async (year, month) => { try { - const datesResult = await ztService.getAvailableDates(); - if (!datesResult.success) { - return { success: false, data: [] }; - } + // 使用后端日历 API 获取包含 top_sector 的完整数据 + const response = await apiRequest(`/api/zt/calendar?year=${year}&month=${month}`); - const dates = datesResult.data.dates || []; - const monthStr = String(month).padStart(2, '0'); - const monthPrefix = `${year}${monthStr}`; - - const calendarData = dates - .filter(d => d.date.startsWith(monthPrefix)) - .map(d => ({ + if (response.success && response.data) { + const calendarData = response.data.map(d => ({ date: d.date, - ztCount: d.count || 0, - formattedDate: d.formatted_date, + ztCount: d.zt_count || 0, topSector: d.top_sector || '', eventCount: d.event_count || 0, indexChange: d.index_change || null, })); - return { success: true, data: calendarData }; + return { success: true, data: calendarData }; + } + + return { success: false, data: [] }; } catch (error) { console.error('获取日历数据失败:', error); return { success: false, data: [] };