diff --git a/src/constants/limitAnalyseTheme.ts b/src/constants/limitAnalyseTheme.ts new file mode 100644 index 00000000..4e0407a1 --- /dev/null +++ b/src/constants/limitAnalyseTheme.ts @@ -0,0 +1,189 @@ +/** + * LimitAnalyse 模块主题常量 + * 统一管理颜色、动画等主题配置 + */ +import { keyframes } from "@emotion/react"; + +// ==================== 颜色系统 ==================== + +/** 黑金主题色系 */ +export const goldColors = { + primary: "#D4AF37", + light: "#F4D03F", + dark: "#B8860B", + glow: "rgba(212, 175, 55, 0.4)", + muted: "rgba(212, 175, 55, 0.6)", +}; + +/** 背景色系 */ +export const bgColors = { + card: "rgba(15, 15, 22, 0.9)", + item: "rgba(0, 0, 0, 0.25)", + hover: "rgba(255, 255, 255, 0.03)", + warning: "rgba(20, 15, 18, 0.95)", +}; + +/** 文字色系 */ +export const textColors = { + primary: "rgba(255, 255, 255, 0.95)", + secondary: "rgba(255, 255, 255, 0.7)", + muted: "rgba(255, 255, 255, 0.5)", + disabled: "rgba(255, 255, 255, 0.3)", +}; + +/** 市场色系 */ +export const marketColors = { + up: "#ef4444", + down: "#22c55e", + flat: "#eab308", + warning: "#f97316", +}; + +/** 边框色系 */ +export const borderColors = { + primary: "rgba(255, 255, 255, 0.06)", + gold: "rgba(212, 175, 55, 0.15)", + hover: "rgba(255, 255, 255, 0.1)", +}; + +// ==================== 风险等级配置 ==================== + +/** 风险等级阈值 */ +export const RISK_THRESHOLDS = { + CRITICAL: 7, // 极高风险 + HIGH: 5, // 高风险 + MEDIUM: 3, // 中风险 + LOW: 2, // 低风险 +} as const; + +/** 风险等级颜色配置 */ +export const RISK_COLORS = { + critical: { + color: "#ef4444", + bg: "rgba(239, 68, 68, 0.2)", + border: "rgba(239, 68, 68, 0.4)", + }, + high: { + color: "#f97316", + bg: "rgba(249, 115, 22, 0.2)", + border: "rgba(249, 115, 22, 0.4)", + }, + medium: { + color: "#eab308", + bg: "rgba(234, 179, 8, 0.2)", + border: "rgba(234, 179, 8, 0.4)", + }, + low: { + color: "#22c55e", + bg: "rgba(34, 197, 94, 0.2)", + border: "rgba(34, 197, 94, 0.4)", + }, +} as const; + +// ==================== 连板样式配置 ==================== + +/** 连板等级配置 */ +export const BOARD_LEVELS = { + DRAGON: 5, // 龙头(5板以上) + HIGH: 3, // 高位(3-4板) + MEDIUM: 2, // 中位(2板) + FIRST: 1, // 首板 +} as const; + +/** 连板颜色配置 */ +export const BOARD_COLORS = { + dragon: { + color: "#ef4444", + bg: "rgba(239, 68, 68, 0.15)", + border: "rgba(239, 68, 68, 0.3)", + }, + high: { + color: "#f97316", + bg: "rgba(249, 115, 22, 0.15)", + border: "rgba(249, 115, 22, 0.3)", + }, + medium: { + color: "#eab308", + bg: "rgba(234, 179, 8, 0.15)", + border: "rgba(234, 179, 8, 0.3)", + }, + first: { + color: "#22c55e", + bg: "rgba(34, 197, 94, 0.15)", + border: "rgba(34, 197, 94, 0.3)", + }, +} as const; + +// ==================== 板块颜色 ==================== + +/** 板块渐变色(前10名) */ +export const SECTOR_GRADIENTS = [ + "linear-gradient(135deg, #ef4444, #dc2626)", + "linear-gradient(135deg, #f97316, #ea580c)", + "linear-gradient(135deg, #eab308, #ca8a04)", + "linear-gradient(135deg, #22c55e, #16a34a)", + "linear-gradient(135deg, #3b82f6, #2563eb)", + "linear-gradient(135deg, #8b5cf6, #7c3aed)", + "linear-gradient(135deg, #ec4899, #db2777)", + "linear-gradient(135deg, #06b6d4, #0891b2)", + "linear-gradient(135deg, #84cc16, #65a30d)", + "linear-gradient(135deg, #f43f5e, #e11d48)", +]; + +/** 板块单色(前10名) */ +export const SECTOR_COLORS = [ + "#ef4444", + "#f97316", + "#eab308", + "#22c55e", + "#3b82f6", + "#8b5cf6", + "#ec4899", + "#06b6d4", + "#84cc16", + "#f43f5e", +]; + +// ==================== 动画定义 ==================== + +/** 闪烁动画 */ +export const shimmer = keyframes` + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +`; + +/** 脉冲动画 */ +export const pulse = keyframes` + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.7; transform: scale(1.05); } +`; + +/** 发光脉冲动画 */ +export const glowPulse = keyframes` + 0%, 100% { box-shadow: 0 0 5px ${goldColors.glow}, 0 0 10px ${goldColors.glow}; } + 50% { box-shadow: 0 0 15px ${goldColors.glow}, 0 0 25px ${goldColors.glow}; } +`; + +/** 淡入动画 */ +export const fadeIn = keyframes` + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +`; + +// ==================== 玻璃样式 ==================== + +/** 玻璃拟态样式 */ +export const glassStyle = { + bg: bgColors.card, + backdropFilter: "blur(20px) saturate(180%)", + border: `1px solid ${borderColors.gold}`, + borderRadius: "16px", +}; + +/** 内部卡片样式 */ +export const innerCardStyle = { + bg: bgColors.item, + border: `1px solid ${borderColors.primary}`, + borderRadius: "12px", + p: 3, +}; diff --git a/src/styles/limitAnalyseStyles.ts b/src/styles/limitAnalyseStyles.ts new file mode 100644 index 00000000..633c68af --- /dev/null +++ b/src/styles/limitAnalyseStyles.ts @@ -0,0 +1,242 @@ +/** + * LimitAnalyse 模块样式定义 + * 统一管理组件样式 + */ +import { goldColors, bgColors, borderColors, textColors } from "@/constants/limitAnalyseTheme"; + +// ==================== 滚动条样式 ==================== + +/** 自定义滚动条样式 */ +export const scrollbarStyles = { + "&::-webkit-scrollbar": { + width: "6px", + }, + "&::-webkit-scrollbar-track": { + background: "rgba(255, 255, 255, 0.02)", + }, + "&::-webkit-scrollbar-thumb": { + background: `${goldColors.primary}60`, + borderRadius: "3px", + }, + "&::-webkit-scrollbar-thumb:hover": { + background: `${goldColors.primary}80`, + }, +}; + +/** 细滚动条样式(4px) */ +export const thinScrollbarStyles = { + "&::-webkit-scrollbar": { + width: "4px", + }, + "&::-webkit-scrollbar-track": { + background: "rgba(255, 255, 255, 0.02)", + }, + "&::-webkit-scrollbar-thumb": { + background: "rgba(239, 68, 68, 0.4)", + borderRadius: "2px", + }, +}; + +// ==================== Ant Design Table 暗色主题 ==================== + +/** Ant Design Table 暗色主题样式 */ +export const antTableDarkStyles = { + ".ant-table": { + background: "transparent !important", + }, + ".ant-table-thead > tr > th": { + background: "rgba(255, 255, 255, 0.03) !important", + borderBottom: "1px solid rgba(255, 255, 255, 0.06) !important", + color: "rgba(255, 255, 255, 0.7) !important", + fontWeight: 600, + fontSize: "12px", + }, + ".ant-table-tbody > tr > td": { + borderBottom: "1px solid rgba(255, 255, 255, 0.04) !important", + color: "rgba(255, 255, 255, 0.9) !important", + background: "transparent !important", + }, + ".ant-table-tbody > tr:hover > td": { + background: "rgba(255, 255, 255, 0.03) !important", + }, + ".ant-table-tbody > tr.ant-table-row-selected > td": { + background: `rgba(212, 175, 55, 0.1) !important`, + }, + ".ant-pagination": { + ".ant-pagination-item": { + background: "transparent", + borderColor: "rgba(255, 255, 255, 0.1)", + a: { color: "rgba(255, 255, 255, 0.7)" }, + }, + ".ant-pagination-item-active": { + borderColor: goldColors.primary, + a: { color: goldColors.primary }, + }, + ".ant-pagination-prev, .ant-pagination-next": { + button: { + color: "rgba(255, 255, 255, 0.5) !important", + }, + }, + }, +}; + +// ==================== 卡片样式 ==================== + +/** 板块卡片样式 */ +export const sectorCardStyles = { + container: { + bg: bgColors.item, + borderRadius: "16px", + border: `1px solid ${borderColors.primary}`, + overflow: "hidden", + transition: "all 0.2s", + _hover: { + borderColor: `${goldColors.primary}30`, + }, + }, + header: { + px: 4, + py: 3, + cursor: "pointer", + transition: "background 0.2s", + _hover: { + bg: bgColors.hover, + }, + }, + expandedHeader: { + bg: bgColors.hover, + }, +}; + +/** 股票项样式 */ +export const stockItemStyles = { + container: { + px: 4, + py: 3, + borderBottom: `1px solid ${borderColors.primary}`, + transition: "all 0.15s", + cursor: "pointer", + _hover: { + bg: bgColors.hover, + }, + }, + selected: { + borderLeft: "3px solid #60a5fa", + bg: "rgba(96, 165, 250, 0.05)", + }, +}; + +// ==================== 统计卡片样式 ==================== + +/** 统计卡片样式 */ +export const statisticsCardStyles = { + container: { + p: 4, + borderRadius: "16px", + bg: bgColors.item, + border: `1px solid ${borderColors.primary}`, + textAlign: "center" as const, + }, + label: { + fontSize: "xs", + color: textColors.muted, + mb: 1, + }, + value: { + fontSize: "2xl", + fontWeight: "700", + }, + unit: { + fontSize: "xs", + color: textColors.disabled, + }, +}; + +// ==================== Badge 样式 ==================== + +/** 风险 Badge 样式 */ +export const riskBadgeStyles = { + high: { + bg: "rgba(239, 68, 68, 0.15)", + color: "#ef4444", + border: "1px solid rgba(239, 68, 68, 0.3)", + }, + medium: { + bg: "rgba(249, 115, 22, 0.15)", + color: "#f97316", + border: "1px solid rgba(249, 115, 22, 0.3)", + }, + low: { + bg: "rgba(234, 179, 8, 0.15)", + color: "#eab308", + border: "1px solid rgba(234, 179, 8, 0.3)", + }, + normal: { + bg: "rgba(34, 197, 94, 0.15)", + color: "#22c55e", + border: "1px solid rgba(34, 197, 94, 0.3)", + }, +}; + +// ==================== 序号徽章样式 ==================== + +/** + * 获取序号徽章样式 + * @param index 序号索引(0-based) + * @param sectorColor 板块颜色 + */ +export function getIndexBadgeStyles(index: number, sectorColor: string) { + const isTop3 = index < 3; + return { + w: "32px", + h: "32px", + borderRadius: "10px", + bg: isTop3 + ? `linear-gradient(135deg, ${sectorColor}, ${sectorColor}88)` + : "rgba(255, 255, 255, 0.1)", + color: isTop3 ? "white" : textColors.secondary, + fontWeight: "bold", + fontSize: "sm", + display: "flex", + justifyContent: "center", + alignItems: "center", + boxShadow: isTop3 ? `0 2px 10px ${sectorColor}40` : "none", + }; +} + +// ==================== 头部样式 ==================== + +/** 组件头部样式 */ +export const headerStyles = { + container: { + px: 4, + py: 3, + borderBottom: `1px solid ${borderColors.primary}`, + }, + title: { + fontSize: "md", + fontWeight: "bold", + color: textColors.primary, + }, + subtitle: { + fontSize: "xs", + color: textColors.muted, + }, +}; + +// ==================== 警告提示样式 ==================== + +/** 风险警告样式 */ +export const riskWarningStyles = { + container: { + mt: 4, + p: 3, + bg: "rgba(234, 179, 8, 0.1)", + borderRadius: "12px", + border: "1px solid rgba(234, 179, 8, 0.2)", + }, + text: { + fontSize: "10px", + color: "#ef4444", + }, +}; diff --git a/src/types/limitAnalyse.ts b/src/types/limitAnalyse.ts new file mode 100644 index 00000000..0c8120c1 --- /dev/null +++ b/src/types/limitAnalyse.ts @@ -0,0 +1,190 @@ +/** + * LimitAnalyse 模块类型定义 + * 涨停分析相关的类型 + */ +import { ComponentType } from "react"; + +// ==================== 股票相关 ==================== + +/** 涨停股票基础信息 */ +export interface LimitUpStock { + /** 股票代码 */ + scode?: string; + /** 股票名称 */ + sname?: string; + /** 备选股票代码 */ + stock_code?: string; + /** 备选股票名称 */ + stock_name?: string; + /** 简化代码 */ + code?: string; + /** 简化名称 */ + name?: string; + /** 涨停时间 'YYYY-MM-DD HH:MM:SS' */ + zt_time?: string; + /** 格式化后的时间 'HH:MM' */ + formatted_time?: string; + /** 涨幅百分比 */ + change_pct?: number; + /** 连板天数 '2', '3', '首板' 等 */ + continuous_days?: string; + /** 首板日期 */ + first_time?: string; + /** 核心板块列表 */ + core_sectors?: string[]; + /** 简要信息 */ + brief?: string; + /** 摘要 */ + summary?: string; + /** 允许额外字段 */ + [key: string]: unknown; +} + +/** 高位股信息 */ +export interface HighPositionStock { + /** 股票代码 */ + stock_code: string; + /** 股票名称 */ + stock_name: string; + /** 当前价格 */ + price: number; + /** 涨幅 */ + increase_rate: number; + /** 连续涨停天数 */ + continuous_limit_up: number; + /** 所属行业 */ + industry: string; + /** 换手率 */ + turnover_rate: number; +} + +// ==================== 板块相关 ==================== + +/** 板块数据 */ +export interface SectorData { + /** 涨停股票数量 */ + count?: number; + /** 板块内股票列表 */ + stocks?: LimitUpStock[]; + /** 净流入金额(亿) */ + net_inflow?: number; + /** 领涨股名称 */ + leading_stock?: string; + /** 股票代码列表 */ + stock_codes?: string[]; + /** 允许额外字段 */ + [key: string]: unknown; +} + +/** 排序后的板块数据 */ +export type SortedSectors = Array<[string, SectorData]>; + +/** 板块排序类型 */ +export type SectorSortType = "count" | "time" | "name" | "change" | "board" | "dragon"; + +// ==================== 风险相关 ==================== + +/** 风险等级配置 */ +export interface RiskLevel { + /** 等级名称 */ + level: string; + /** 主色 */ + color: string; + /** 背景色 */ + bg: string; + /** 边框色 */ + border: string; + /** 图标组件 */ + icon: ComponentType; + /** 状态描述 */ + status: string; +} + +/** 高位股统计数据 */ +export interface HighPositionStatistics { + /** 高位股总数 */ + total_count: number; + /** 平均连板天数 */ + avg_continuous_days: number; + /** 最高连板天数 */ + max_continuous_days: number; +} + +/** 高位股数据 */ +export interface HighPositionData { + /** 股票列表 */ + stocks: HighPositionStock[]; + /** 统计信息 */ + statistics: HighPositionStatistics; +} + +// ==================== 组件 Props ==================== + +/** SectorMovementTable Props */ +export interface SectorMovementTableProps { + /** 排序后的板块数据 */ + sortedSectors: SortedSectors; + /** 总涨停股票数 */ + totalStocks: number; + /** 活跃板块数量 */ + activeSectorCount: number; + /** 当前选中的板块 */ + selectedSector: string | null; + /** 板块选中回调 */ + onSectorSelect: (sector: string) => void; +} + +/** HighPositionSidebar Props */ +export interface HighPositionSidebarProps { + /** 日期字符串 */ + dateStr: string; +} + +/** HighPositionStocks Props */ +export interface HighPositionStocksProps { + /** 日期字符串 */ + dateStr: string; +} + +/** UnifiedSectorCard Props */ +export interface UnifiedSectorCardProps { + /** 排序后的板块数据 */ + sortedSectors?: SortedSectors; + /** 总涨停股票数 */ + totalStocks?: number; + /** 展示模式: accordion(手风琴)或 card(卡片) */ + displayMode?: "accordion" | "card"; +} + +// ==================== 股票角色 ==================== + +/** 股票角色类型 */ +export type StockRoleType = "dragon" | "follow" | "rebound" | "first" | "normal"; + +/** 股票角色配置 */ +export interface StockRole { + /** 角色标签 */ + label: string; + /** 主色 */ + color: string; + /** 背景色 */ + bg: string; + /** 图标组件 */ + icon: ComponentType; +} + +// ==================== 连板样式 ==================== + +/** 连板样式配置 */ +export interface BoardStyle { + /** 背景色 */ + bg: string; + /** 文字色 */ + color: string; + /** 边框色 */ + border: string; + /** 标签文字 */ + label: string; + /** 是否显示火焰图标 */ + showFlame: boolean; +} diff --git a/src/utils/limitAnalyseUtils.ts b/src/utils/limitAnalyseUtils.ts new file mode 100644 index 00000000..0106bc84 --- /dev/null +++ b/src/utils/limitAnalyseUtils.ts @@ -0,0 +1,291 @@ +/** + * LimitAnalyse 模块工具函数 + * 统一管理风险判断、样式获取等逻辑 + */ +import { AlertTriangle, Eye, Activity, Flame, Crown, TrendingUp, Zap, Circle } from "lucide-react"; +import type { RiskLevel, BoardStyle, StockRole, LimitUpStock } from "@/types/limitAnalyse"; +import { + RISK_THRESHOLDS, + RISK_COLORS, + BOARD_LEVELS, + BOARD_COLORS, + SECTOR_COLORS, + SECTOR_GRADIENTS, + goldColors, +} from "@/constants/limitAnalyseTheme"; + +// ==================== 风险等级判断 ==================== + +/** + * 根据连板天数获取风险等级配置 + * @param days 连板天数 + * @returns 风险等级配置 + */ +export function getRiskLevel(days: number): RiskLevel { + if (days >= RISK_THRESHOLDS.CRITICAL) { + return { + level: "极高", + color: RISK_COLORS.critical.color, + bg: RISK_COLORS.critical.bg, + border: RISK_COLORS.critical.border, + icon: AlertTriangle, + status: "缩量一字,高风险", + }; + } + if (days >= RISK_THRESHOLDS.HIGH) { + return { + level: "高", + color: RISK_COLORS.high.color, + bg: RISK_COLORS.high.bg, + border: RISK_COLORS.high.border, + icon: AlertTriangle, + status: "放量分歧,需观察", + }; + } + if (days >= RISK_THRESHOLDS.MEDIUM) { + return { + level: "中", + color: RISK_COLORS.medium.color, + bg: RISK_COLORS.medium.bg, + border: RISK_COLORS.medium.border, + icon: Eye, + status: "正常波动", + }; + } + return { + level: "低", + color: RISK_COLORS.low.color, + bg: RISK_COLORS.low.bg, + border: RISK_COLORS.low.border, + icon: Activity, + status: "健康", + }; +} + +/** + * 计算整体风险评估 + * @param statistics 统计数据 + * @returns 风险评估结果 + */ +export function getRiskAssessment(statistics: { + avg_continuous_days?: number; + max_continuous_days?: number; + total_count?: number; +}): { level: string; color: string } { + const avgDays = statistics?.avg_continuous_days || 0; + const maxDays = statistics?.max_continuous_days || 0; + const totalCount = statistics?.total_count || 0; + + const score = avgDays * 2 + maxDays * 0.5 + totalCount * 0.3; + + if (score >= 20) return { level: "高风险", color: RISK_COLORS.critical.color }; + if (score >= 12) return { level: "中风险", color: RISK_COLORS.high.color }; + if (score >= 6) return { level: "偏高", color: RISK_COLORS.medium.color }; + return { level: "正常", color: RISK_COLORS.low.color }; +} + +// ==================== 连板样式 ==================== + +/** + * 根据连板天数获取样式配置 + * @param days 连板天数 + * @returns 连板样式配置 + */ +export function getBoardStyle(days: number): BoardStyle { + if (days >= BOARD_LEVELS.DRAGON) { + return { + bg: BOARD_COLORS.dragon.bg, + color: BOARD_COLORS.dragon.color, + border: BOARD_COLORS.dragon.border, + label: `${days}连板`, + showFlame: true, + }; + } + if (days >= BOARD_LEVELS.HIGH) { + return { + bg: BOARD_COLORS.high.bg, + color: BOARD_COLORS.high.color, + border: BOARD_COLORS.high.border, + label: `${days}连板`, + showFlame: days >= 4, + }; + } + if (days >= BOARD_LEVELS.MEDIUM) { + return { + bg: BOARD_COLORS.medium.bg, + color: BOARD_COLORS.medium.color, + border: BOARD_COLORS.medium.border, + label: `${days}连板`, + showFlame: false, + }; + } + return { + bg: BOARD_COLORS.first.bg, + color: BOARD_COLORS.first.color, + border: BOARD_COLORS.first.border, + label: "首板", + showFlame: false, + }; +} + +/** + * 获取连板天数的颜色 + * @param days 连板天数 + * @returns 颜色值 + */ +export function getContinuousDaysColor(days: number): string { + if (days >= BOARD_LEVELS.DRAGON) return BOARD_COLORS.dragon.color; + if (days >= BOARD_LEVELS.HIGH) return BOARD_COLORS.high.color; + if (days >= BOARD_LEVELS.MEDIUM) return BOARD_COLORS.medium.color; + return BOARD_COLORS.first.color; +} + +// ==================== 时间格式化 ==================== + +/** + * 格式化涨停时间 + * @param stock 股票对象 + * @returns 格式化后的时间字符串 + */ +export function formatLimitUpTime(stock: LimitUpStock): string { + if (stock.formatted_time) return stock.formatted_time; + if (stock.zt_time) { + const time = stock.zt_time.split(" ")[1]; + return time || stock.zt_time; + } + return "-"; +} + +/** + * 解析连板天数 + * @param continuous_days 连板字符串,如 "2", "3", "首板" + * @returns 连板天数 + */ +export function parseContinuousDays(continuous_days?: string): number { + if (!continuous_days) return 1; + if (continuous_days === "首板") return 1; + const num = parseInt(continuous_days); + return isNaN(num) ? 1 : num; +} + +// ==================== 板块颜色 ==================== + +/** + * 获取板块颜色 + * @param index 板块索引(0-based) + * @returns 颜色值 + */ +export function getSectorColor(index: number): string { + if (index < SECTOR_COLORS.length) { + return SECTOR_COLORS[index]; + } + return goldColors.primary; +} + +/** + * 获取板块渐变色 + * @param index 板块索引(0-based) + * @returns 渐变色值 + */ +export function getSectorGradient(index: number): string { + if (index < SECTOR_GRADIENTS.length) { + return SECTOR_GRADIENTS[index]; + } + return `linear-gradient(135deg, ${goldColors.primary}, ${goldColors.dark})`; +} + +// ==================== 股票角色判断 ==================== + +/** 股票角色配置 */ +export const STOCK_ROLES: Record = { + dragon: { + label: "龙头", + color: "#ef4444", + bg: "rgba(239, 68, 68, 0.15)", + icon: Crown, + }, + follow: { + label: "跟风", + color: "#f97316", + bg: "rgba(249, 115, 22, 0.15)", + icon: TrendingUp, + }, + rebound: { + label: "反包", + color: "#8b5cf6", + bg: "rgba(139, 92, 246, 0.15)", + icon: Zap, + }, + first: { + label: "首板", + color: "#22c55e", + bg: "rgba(34, 197, 94, 0.15)", + icon: Flame, + }, + normal: { + label: "", + color: "rgba(255, 255, 255, 0.6)", + bg: "transparent", + icon: Circle, + }, +}; + +/** + * 判断股票角色 + * @param stock 股票对象 + * @param sectorStocks 板块内所有股票 + * @param sectorIndex 板块索引 + * @returns 股票角色配置 + */ +export function getStockRole( + stock: LimitUpStock, + sectorStocks: LimitUpStock[], + sectorIndex: number +): StockRole { + const boardDays = parseContinuousDays(stock.continuous_days); + + // 龙头判断:5板以上 或 板块第一名且3板以上 + if (boardDays >= 5) { + return STOCK_ROLES.dragon; + } + + // 首板判断 + if (boardDays === 1) { + return STOCK_ROLES.first; + } + + // 跟风判断:在热门板块(前3)且2-4板 + if (sectorIndex < 3 && boardDays >= 2 && boardDays < 5) { + // 如果是板块内涨停时间最早的,可能是龙头 + const sortedByTime = [...sectorStocks].sort((a, b) => + (a.zt_time || "").localeCompare(b.zt_time || "") + ); + if (sortedByTime[0]?.scode === stock.scode && boardDays >= 3) { + return STOCK_ROLES.dragon; + } + return STOCK_ROLES.follow; + } + + return STOCK_ROLES.normal; +} + +// ==================== 排序策略 ==================== + +/** 排序策略配置 */ +export const SORT_STRATEGIES = { + /** 按涨幅排序 */ + change: (a: LimitUpStock, b: LimitUpStock) => + (parseFloat(String(b.change_pct)) || 0) - (parseFloat(String(a.change_pct)) || 0), + + /** 按连板排序 */ + board: (a: LimitUpStock, b: LimitUpStock) => + parseContinuousDays(b.continuous_days) - parseContinuousDays(a.continuous_days), + + /** 按涨停时间排序 */ + time: (a: LimitUpStock, b: LimitUpStock) => + (a.zt_time || "").localeCompare(b.zt_time || ""), + + /** 按名称排序 */ + name: (a: LimitUpStock, b: LimitUpStock) => + (a.sname || "").localeCompare(b.sname || ""), +};