通达信插件-“涨停炸板时间轴”,用通用浏览器调用

拾荒网 涨停板 508

牛人开发的通达信插件,可盘中依据涨停时间显示涨停或炸板的个股信息。

个股所属概念,调用KPL数据。

通过通达信的“通用浏览器”调用使用。

界面如图:

使用方法步骤:

1、把下面源代码复制到“记事本”,并保存为.html格式,命名为“涨停炸板时间轴.html”

2、“涨停炸板时间轴.html”可存放于本机任意位置。

3、在通达信自定义版面,新建“通用浏览器”,在地址上选取该““涨停炸板时间轴.html””文件即可。

“涨停炸板时间轴.html”源代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>涨停时间轴</title>
    <style>
        body {
            font-family: "Microsoft YaHei", sans-serif;
            margin: 0px;
            background: #000;
            color: #fff;
            padding-top: 90px;
        }
        .controls {
            display: flex;
            flex-direction: column;
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            background: #000;
            padding: 2px;
            z-index: 1000;
            box-shadow: 0 2px 5px rgba(0,0,0,0.5);
        }
        .market-overview {
            width: 100%;
            padding: 2px 0;
            border-bottom: 1px solid #333;
            margin-bottom: 1px;
        }
        .market-row {
            display: flex;
            justify-content: center;
            flex-wrap: wrap;
            font-size: 14px;
            line-height: 1.5;
        }
        .stats-bar {
            padding: 1px 2px;
            background: #222;
            display: flex;
            flex-direction: column;
            font-size: 14px;
            display: flex;
            gap: 5px;
            border-bottom: 1px solid #333;
        }

        .market-row span {
            margin: 0 8px;
            color: #fff;
        }
        .market-row .red { color: #ff0000; }
        .market-row .green { color: #00ff00; }
        .market-row .white { color: #ffffff; }
        .market-row .highlight { color: #ffd700; }
        .market-row .blue { color: #00bfff; }
        .original-controls {
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 2px;
            padding-top: 2px;
        }
        .type-label {
            color: #ffd700;
            font-size: 18px;
            margin-right: 2px;
            white-space: nowrap;
        }
        #cztCount { color: #00ff00; font-size: 0.8em; margin-left: 1px; }
        #ztCount { color: #ff0000; font-size: 0.8em; margin-left: 1px; }
        .date-control {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .date-btn {
            background: none;
            border: 1px solid #FFD700;
            color: #FFD700;
            padding: 2px 6px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
        }
        .refresh-btn {
            background: #FFD700;
            color: #1a1a1a;
            border: none;
            padding: 2px 6px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
        }
        .timeline {
            position: relative;
            max-width: 1200px;
            margin: 0 auto;
            padding: 0 0px;
            margin-top: 65px;
        }
        .timeline::before {
            content: '';
            position: absolute;
            left: 50%;
            transform: translateX(-50%);
            top: 0;
            bottom: 0;
            width: 4px;
            background: #ffd700;
            box-shadow: 0 0 10px #ffd700;
        }
        .timeline-item {
            position: relative;
            margin-bottom: 2px;
            display: flex;
            justify-content: space-between;
            width: 100%;
        }
        .time-marker.red { color: #ff0000; text-shadow: 0 0 8px #ff0000; }
        .time-marker.white { color: #ffffff; text-shadow: 0 0 8px #ffffff; }
        .time-marker.purple-red { color: #ff00ff; text-shadow: 0 0 8px #ff00ff; }
        .time-marker {
            position: absolute;
            left: 50%;
            transform: translateX(-50%);
            top: 0;
            width: 50px;
            text-align: center;
            font-size: 16px;
            font-weight: bold;
        }
        .left-panel,
        .right-panel {
            width: 45%;
            min-height: 20px;
            padding: 2px;
            background: #1a1a1a;
            border-radius: 6px;
            border: 1px solid #333;
        }
        .stock-item {
            display: grid;
            grid-template-columns: minmax(max-content, auto) minmax(max-content, auto) minmax(max-content, 1fr);
            gap: 2px;
            padding: 2px;
            margin: 2px 0;
            align-items: center;
            background: #2a2a2a;
            border-radius: 4px;
            border-left: 2px solid #ffd700;
        }
        .stock-name {
            font-weight: bold;
            white-space: nowrap;
            overflow: visible;
            text-overflow: clip;
            color: #ffd700;
            text-decoration: none;
            font-size: 14px;
            min-width: max-content;
        }
        .stock-name:hover {
            color: white;
            text-shadow: 0 0 8px #ffd700;
        }
        .board-status {
            text-align: center;
            color: #ffd700;
            font-size: 14px;
            padding: 1px 2px;
            border-radius: 3px;
            min-width: max-content;
        }

        .stock-item.has-yesterday {
            grid-template-columns: minmax(max-content, auto) minmax(max-content, auto) minmax(max-content, auto) minmax(max-content, 1fr);
        }
        .stock-item.has-fengdan {
            grid-template-columns: minmax(max-content, auto) minmax(max-content, auto) minmax(max-content, auto) minmax(max-content, 1fr);
        }
        .yesterday-dayb {
            font-size: 12px;
            color: #888;
            padding: 0 3px;
            min-width: 20px;
            text-align: center;
        }
        .reason-box {
            cursor: pointer;
            padding: 2px;
            border-radius: 3px;
            transition: all 0.3s;
            font-size: 12px;
            white-space: nowrap;
            overflow: visible;
            text-overflow: clip;
            background: rgba(255,255,255,0.1);
            min-width: max-content;
        }
        .fengdan-box {
            font-size: 12px;
            color: #ffd700;
            padding: 0 3px;
            text-align: right;
            min-width: max-content;
        }
        @keyframes glow {
            0% { box-shadow: 0 0 5px currentColor; }
            50% { box-shadow: 0 0 20px currentColor; }
            100% { box-shadow: 0 0 5px currentColor; }
        }
    </style>
</head>
<body>
    <div>
        <div>
            <div id="statsBar">
                <div>板块:<span id="reasonCount">0</span></div>
                <div>连板:<span id="daybCount">0</span></div>
            </div>
            <div id="marketRow1"></div>
            <div id="marketRow2"></div>
            <div id="marketRow3"></div>
        </div>
        <div>
            <span>
                炸板<span id="cztCount">0</span>|涨停<span id="ztCount">0</span>
            </span>
            <div>
                <button id="prev-day">&lt;</button>
                <span id="current-date"></span>
                <button id="next-day">&gt;</button>
                <button id="refreshBtn">刷新</button>
            </div>
        </div>
    </div>

    <div id="timeline"></div>

    <script>
        function getBeijingDate() {
            const now = new Date();
            return new Date(now.getTime() + (now.getTimezoneOffset() * 60000) + (3600000 * 8));
        }

        function isWorkday(date) {
            const day = date.getDay();
            return day !== 0 && day !== 6;
        }

        function getLastWorkday() {
            const now = getBeijingDate();
            const isTodayWorkday = isWorkday(now);
            const currentHour = now.getHours();
            const currentMinute = now.getMinutes();

            if (isTodayWorkday) {
                if (currentHour > 9 || (currentHour === 9 && currentMinute >= 15)) {
                    return now;
                }
            }
            
            let lastWorkday = new Date(now);
            do {
                lastWorkday.setDate(lastWorkday.getDate() - 1);
            } while (!isWorkday(lastWorkday));
            
            return lastWorkday;
        }

        function adjustDate(increment) {
            const step = increment ? 1 : -1;
            let newDate = new Date(currentDate);
            do {
                newDate.setDate(newDate.getDate() + step);
            } while (!isWorkday(newDate));
            
            const lastWorkday = getLastWorkday();
            if(newDate > lastWorkday) newDate = new Date(lastWorkday);
            currentDate = newDate;
            updateDateDisplay();
            renderTimeline(); // 确保日期变化后刷新数据
        }

        function formatAPIDate(date) {
            const y = date.getFullYear();
            const m = String(date.getMonth() + 1).padStart(2, '0');
            const d = String(date.getDate()).padStart(2, '0');
            return `${y}${m}${d}`;
        }

        function formatDisplayDate(date) {
            const y = date.getFullYear();
            const m = String(date.getMonth() + 1).padStart(2, '0');
            const d = String(date.getDate()).padStart(2, '0');
            const weekdays = ['日','一','二','三','四','五','六'];
            return `${y}${m}${d}周${weekdays[date.getDay()]}`;
        }

        function updateDateDisplay() {
            document.getElementById('current-date').textContent = formatDisplayDate(currentDate);
        }

        function isTradingTime() {
            const now = getBeijingDate();
            const hours = now.getHours();
            const minutes = now.getMinutes();
            const time = hours * 100 + minutes;
            return (time >= 915 && time <= 1131) || (time >= 1259 && time <= 1501);
        }

        function setupAutoRefresh() {
            const now = getBeijingDate();
            if(isWorkday(now) && isTradingTime()) {
                if(!refreshInterval) {
                    refreshInterval = setInterval(renderTimeline, 5000);
                }
            } else {
                clearInterval(refreshInterval);
                refreshInterval = null;
            }
        }

        // 初始化当前日期
        let currentDate = getLastWorkday();
        let refreshInterval = null;

        async function fetchMarketData(date) {
            const aday = formatAPIDate(date);
            try {
                const [marketRes, blockRes] = await Promise.all([
                    fetch(`https://data.10jqka.com.cn/mobileapi/hotspot_focus/market_state/v1/overview?date=${aday}`),
                    fetch(`https://goserver.huanshoulv.com/aimapp/surgeLimit/surgeTopBlock?date=${aday}&sort_field_name=surgedays&sort_type=-1&ts=1740497791359&sign=77e7a16f153f39995bdb487c3e0e79d4&appversion=2.0.0.6&has_dep=false&channel=hsl-app&div=ANDH020006`)
                ]);

                const [marketData, blockData] = await Promise.all([
                    marketRes.json(),
                    blockRes.json()
                ]);

                const rise = marketData.data.rise_fall.rise;
                const fall = marketData.data.rise_fall.fall;
                const deuce = marketData.data.rise_fall.deuce;
                const zt = marketData.data.rise_fall.limit_up;
                const dt = marketData.data.rise_fall.limit_down;
                const jcje = marketData.data.turnover.now;
                const zcje = marketData.data.turnover.pre;
                const jhb = marketData.data.limit_up.now;
                const zhb = marketData.data.limit_up.pre;

                const blocks = blockData.data.block.slice(0, 3)
                    .map(b => `${b.block_name} ${b.weight}`)
                    .join('  ');

                return { rise, fall, deuce, zt, dt, jcje, zcje, jhb, zhb, blocks };
            } catch (error) {
                console.error('市场数据加载失败:', error);
                return null;
            }
        }

        async function getHSL(aday) {
            try {
                const headers = new Headers({'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'});
                
                const mainUrl = `https://goserver.huanshoulv.com/aimapp/surgeLimit/surgeTopBlock?date=${aday}&sort_field_name=surgedays&sort_type=-1&ts=1740497791359&sign=77e7a16f153f39995bdb487c3e0e79d4&appversion=2.0.0.6&has_dep=false&channel=hsl-app&div=ANDH020006`;
                const mainRes = await fetch(mainUrl, {headers});
                const mainData = await mainRes.json();
                
                const stocks = {};
                const blocks = mainData.data.block;

                for (const block of blocks) {
                    const bkname = encodeURIComponent(block.block_name);
                    const bkzt = block.weight;
                    
                    const blockUrl = `https://goserver.huanshoulv.com/aimapp/surgeLimit/surgeTopBlock?date=${aday}&sort_field_name=surgedays&block_name=${bkname}&sort_type=-1&ts=1740497791359&sign=77e7a16f153f39995bdb487c3e0e79d4&appversion=2.0.0.6&has_dep=false&channel=hsl-app&div=ANDH020006`;
                    
                    const blockRes = await fetch(blockUrl, {headers});
                    const blockData = await blockRes.json();
                    
                    blockData.data.surgeList.forEach(asure => {
                        const code = asure[0];
                        const fdl = asure[7];
                        const nowc = asure[3];
                        const fdValue = fdl * nowc;
                        
                        stocks[code] = {
                            concept: block.block_name,
                            lb: asure[26],
                            fd: fdValue > 100000000 
                                ? `${(fdValue/100000000).toFixed(2)}亿`
                                : `${Math.round(fdValue/10000)}万`
                        };
                    });
                }
                return stocks;
            } catch (error) {
                console.error('HSL数据获取失败:', error);
                return {};
            }
        }

async function getHSLztyy(aday) {
    try {
        const headers = new Headers({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'
        });

        const isLatestTradingDay = await checkIsLatestTradingDay(aday);
        const url1 = isLatestTradingDay 
            ? `http://goserver.huanshoulv.com/aimapp/surgeLimit/surgeLimitList?page=1&type=1&sort_type=-1&page_count=5000&channel=hsl-app&ts=1560867041083&sign=8e4ef91d04fca0817e2b21e9c816f14e`
            : `http://goserver.huanshoulv.com/aimapp/surgeLimit/surgeLimitHistory?date=${aday}&history_type=1&page=1&type=1&sort_type=-1&page_count=5000&channel=hsl-app&ts=1560867041083&sign=8e4ef91d04fca0817e2b21e9c816f14e`;
        
        const url2 = isLatestTradingDay 
            ? `http://goserver.huanshoulv.com/aimapp/surgeLimit/surgeLimitList?page=1&type=2&sort_type=-1&page_count=5000&channel=hsl-app&ts=1560867041083&sign=8e4ef91d04fca0817e2b21e9c816f14e`
            : `http://goserver.huanshoulv.com/aimapp/surgeLimit/surgeLimitHistory?date=${aday}&history_type=1&page=1&type=2&sort_type=-1&page_count=5000&channel=hsl-app&ts=1560867041083&sign=8e4ef91d04fca0817e2b21e9c816f14e`;
        
        const [res1, res2] = await Promise.all([
            fetch(url1, { headers }), 
            fetch(url2, { headers })
        ]);
        
        const data1 = await res1.json();
        const data2 = await res2.json();
        
        const list = [...data1.data.surgeList, ...data2.data.surgeList];
        const hslztyy = {};
        
        function parseReason(str) {
            if (!str) return null;
            const matchPlus = str.match(/看多\+(.*?)\(/);
            if (matchPlus) return matchPlus[1];
            const matchStart = str.match(/^(.*?)\(/);
            return matchStart ? matchStart[1] : str.split('+')[0]; // 如果都不匹配,取第一个概念
        }

        list.forEach(item => {
            const code = item[32] || item[0]; // 兼容不同接口可能的不同字段位置
            const reason = parseReason(item[3]) || '未知概念';
            hslztyy[code] = reason;
        });
        
        return hslztyy;
    } catch (error) {
        console.error('HSL涨停原因获取失败:', error);
        return {};
    }
}

        function getLastday() {
            const now = getBeijingDate();
            const isTodayWorkday = isWorkday(now);
            const currentHour = now.getHours();
            const currentMinute = now.getMinutes();

            if (isTodayWorkday) {
                    return formatAPIDate(now);
            }
            let lastWorkday = new Date(now);
            do {
                lastWorkday.setDate(lastWorkday.getDate() - 1);
            } while (!isWorkday(lastWorkday));
            return formatAPIDate(lastWorkday);
        }

async function checkIsLatestTradingDay(dateStr) {
     return dateStr ===  getLastday();
}


        // 主数据接口
        async function fetchData(date) {
            const currentDateObj = new Date(date);
            const formattedDate = formatAPIDate(currentDateObj);
            
            // 获取昨日数据
            const prevDateObj = new Date(currentDateObj);
            prevDateObj.setDate(prevDateObj.getDate() -1);
            const prevDate = getLastWorkday(prevDateObj);
            const prevFormattedDate = formatAPIDate(prevDate);

            try {
                const [ztRes, cztRes, hslData, hslztyyData, prevRes] = await Promise.all([
                    fetch(`https://api.duishu.com//lhbapp/zhangting/index?day=${formattedDate}&pagecount=20&position=2&page=1&type=1&orderKey=zt_time&orderValue=desc&apiversion=8.9`),
                    fetch(`https://api.duishu.com//lhbapp/zhangting/index?day=${formattedDate}&pagecount=20&page=1&type=2&apiversion=8.9`),
                    getHSL(formattedDate),
                    getHSLztyy(formattedDate),
                    fetch(`https://api.duishu.com//lhbapp/zhangting/index?day=${prevFormattedDate}&pagecount=200&position=2&page=1&type=1&orderKey=zt_time&orderValue=desc&apiversion=8.9`)
                ]);

                const [ztData, cztData, prevData] = await Promise.all([
                    ztRes.json(),
                    cztRes.json(),
                    prevRes.json().catch(() => ({ data: { stock_list: { list: [] }}})) // 容错处理
                ]);

                const prevDaybMap = prevData.data.stock_list.list.reduce((acc, item) => {
                    const code = item[0].text.split('#')[1];
                    acc[code] = item[3].text.replace('连', '').replace('首', '1');
                    return acc;
                }, {});

                const processItems = (items, isZt) => items.map(item => {
                    const code = item[0].text.split('#')[1];
                    let reason = hslData[code]?.concept || '未知概念';
                    if (reason === '未知概念' && hslztyyData[code]) {
                        reason = hslztyyData[code];
                    }
                    return {
                        time: item[2].text,
                        name: item[0].text.split('#')[0],
                        code: code,
                        zf: item[1].text,
                        reason: reason,
                        dayb: item[3].text.replace('连', '').replace('首', '1'),
                        prev_dayb: prevDaybMap[code] || '-',
                        fengdan: item[5].text,
                        type: isZt ? 'zt' : 'czt'
                    };
                });

                return [
                    ...processItems(ztData.data.stock_list.list, true),
                    ...processItems(cztData.data.stock_list.list, false)
                ];
            } catch (error) {
                console.error('数据获取失败:', error);
                return [];
            }
        }

        function createStockItem(item, isCzt) {
            const div = document.createElement('div');
            div.className = `stock-item ${isCzt ? 'has-yesterday' : 'has-fengdan'}`;
            div.innerHTML = `
                <a href="http://www.treeid/code_${item.code}">
                    ${item.name}
                </a>
                <div class="board-status selectable"
                     data-dayb="${item.dayb}"
                     style="--hue:${Math.random() * 360}">
                    ${isCzt ? item.zf : item.dayb}
                </div>
                ${isCzt ? `
                <div title="昨日连板天数">
                    ${item.prev_dayb}
                </div>
                ` : ''}
                <div class="reason-box selectable"
                     data-reason="${item.reason}"
                     style="--hue:${Math.random() * 360}">
                    ${item.reason}
                </div>
                ${!isCzt ? `
                <div title="封单金额">
                    ${item.fengdan}
                </div>
                ` : ''}
            `;
            return div;
        }

        async function renderTimeline() {
            try {
                const marketData = await fetchMarketData(currentDate);
                if (marketData) {
                    const jcjeNum = parseFloat(marketData.jcje.replace(/,/g, '')) || 0;
                    const zcjeNum = parseFloat(marketData.zcje.replace(/,/g, '')) || 0;
                    const jhbNum = parseInt(marketData.jhb) || 0;
                    const zhbNum = parseInt(marketData.zhb) || 0;

                    document.getElementById('marketRow1').innerHTML = `
                        <span>上涨<span>${marketData.rise}</span>
                        下跌<span>${marketData.fall}</span>
                        平盘<span>${marketData.deuce}</span>
                        涨停<span>${marketData.zt}</span>
                        跌停<span>${marketData.dt}</span>
                    `;

                    document.getElementById('marketRow2').innerHTML = `
                        <span>今额:<span class="${jcjeNum > zcjeNum ? 'red' : 'green'}">${marketData.jcje}</span>
                        昨额:<span>${marketData.zcje}</span>
                        今板高<span class="${jhbNum > zhbNum ? 'red' : 'green'}">${marketData.jhb}</span>
                        昨板高<span>${marketData.zhb}</span>
                    `;

                    document.getElementById('marketRow3').innerHTML = 
                        marketData.blocks.split('  ').map(b => 
                            `<span>${b.split(' ')[0]} <span>${b.split(' ')[1]}</span></span>`
                        ).join('');
                }

                const data = await fetchData(currentDate);
                let ztCount = 0, cztCount = 0;
                const sortedData = data.sort((a, b) => {
                    const timeA = parseInt(a.time.replace(':', ''));
                    const timeB = parseInt(b.time.replace(':', ''));
                    return timeB - timeA;
                });

                const groupedData = sortedData.reduce((acc, item) => {
                    item.type === 'zt' ? ztCount++ : cztCount++;
                    acc[item.time] = acc[item.time] || { zt: [], czt: [] };
                    item.type === 'zt' ? acc[item.time].zt.push(item) : acc[item.time].czt.push(item);
                    return acc;
                }, {});

                document.getElementById('ztCount').textContent = ztCount;
                document.getElementById('cztCount').textContent = cztCount;

                const timeline = document.getElementById('timeline');
                timeline.innerHTML = '';
                Object.entries(groupedData).forEach(([time, items]) => {
                    const [hours, minutes] = time.split(':').map(Number);
                    const totalTime = hours * 100 + minutes;
                    
                    let timeColorClass = '';
                    if (totalTime < 1000) {
                        timeColorClass = 'red';
                    } else if (totalTime >= 1000 && totalTime < 1430) {
                        timeColorClass = 'white';
                    } else {
                        timeColorClass = 'purple-red';
                    }

                    const container = document.createElement('div');
                    container.className = 'timeline-item';
                    container.innerHTML = `
                        <div class="time-marker ${timeColorClass}">${time}</div>
                        <div>
                            ${items.czt.map(item => createStockItem(item, true).outerHTML).join('')}
                        </div>
                        <div>
                            ${items.zt.map(item => createStockItem(item, false).outerHTML).join('')}
                        </div>
                    `;
                    timeline.appendChild(container);
                });

                const restoreSelection = (storageKey, dataAttr) => {
                    const selections = JSON.parse(sessionStorage.getItem(storageKey) || '[]');
                    if (selections.length) {
                        requestAnimationFrame(() => {
                            let needUpdate = false;
                            selections.forEach(({ value, color }) => {
                                document.querySelectorAll(`[data-${dataAttr}="${value}"]`).forEach(box => {
                                    box.style.backgroundColor = color;
                                    box.style.color = '#000';
                                    box.style.fontWeight = 'bold';
                                    box.style.animation = 'glow 1.5s ease-in-out infinite';
                                    needUpdate = true;
                                });
                            });
                            if(needUpdate) updateCount();
                        });
                    }
                    updateCount();
                };

                restoreSelection('selections', 'reason');
                restoreSelection('daybSelections', 'dayb');
            } catch (error) {
                console.error('数据加载失败:', error);
            }
        }

        function updateCount() {
            const reasonCounts = {};
            document.querySelectorAll('.reason-box[data-reason]').forEach(item => {
                if (item.style.backgroundColor) {
                    const reason = item.dataset.reason.split(' ')[0];
                    reasonCounts[reason] = (reasonCounts[reason] || 0) + 1;
                }
            });
            
            const sortedReasons = Object.entries(reasonCounts)
                .sort((a, b) => b[1] - a[1])
                .slice(0, 5)
                .map(([k, v]) => `${k}${v}`);
            
            document.getElementById('reasonCount').textContent = sortedReasons.join(' ') || '0';

            const daybCounts = {};
            document.querySelectorAll('.board-status[data-dayb]').forEach(item => {
                if (item.style.backgroundColor) {
                    const dayb = item.dataset.dayb;
                    daybCounts[dayb] = (daybCounts[dayb] || 0) + 1;
                }
            });
            const sortedDayb = Object.entries(daybCounts)
                .sort((a, b) => b[0].localeCompare(a[0]))
                .map(([k, v]) => `${k}${v}`);
            
            document.getElementById('daybCount').textContent = sortedDayb.join(' ') || '0';
        }

        // 事件监听器
        document.getElementById('prev-day').onclick = () => adjustDate(false);
        document.getElementById('next-day').onclick = () => adjustDate(true);
        document.getElementById('refreshBtn').onclick = renderTimeline;

        document.getElementById('timeline').addEventListener('click', (e) => {
            const target = e.target.closest('.selectable');
            if (!target) return;

            const isDayb = target.classList.contains('board-status');
            const storageKey = isDayb ? 'daybSelections' : 'selections';
            const dataAttr = isDayb ? 'dayb' : 'reason';

            const selections = JSON.parse(sessionStorage.getItem(storageKey) || '[]');
            const currentColor = `hsl(${target.style.getPropertyValue('--hue')},100%,50%)`;
            const value = target.dataset[dataAttr];

            const existingIndex = selections.findIndex(s => s.value === value);
            if (existingIndex >= 0) {
                selections.splice(existingIndex, 1);
                document.querySelectorAll(`[data-${dataAttr}="${value}"]`).forEach(el => {
                    el.style.backgroundColor = '';
                    el.style.color = '';
                    el.style.animation = '';
                });
            } else {
                if (selections.length >= 200) selections.shift();
                selections.push({ value, color: currentColor });
                document.querySelectorAll(`[data-${dataAttr}="${value}"]`).forEach(el => {
                    el.style.backgroundColor = currentColor;
                    el.style.color = '#000';
                    el.style.fontWeight = 'bold';
                    el.style.animation = 'glow 1.5s ease-in-out infinite';
                });
            }
            sessionStorage.setItem(storageKey, JSON.stringify(selections));
            updateCount();
        });

        // 初始化
        updateDateDisplay();
        renderTimeline();
        setInterval(setupAutoRefresh, 60000);
        setupAutoRefresh();
    </script>
</body>
</html>


标签: 涨停时间轴