台股浮窗(TWSE API)

顯示台股主要股票資訊,每5分鐘更新,可顯示昨收價,漲跌顯色,整齊排版,可以自己在STOCKS新增股票,就可以無限增加。

// ==UserScript==
// @name         台股浮窗(TWSE API)
// @namespace    issac
// @version      1.0
// @description  顯示台股主要股票資訊,每5分鐘更新,可顯示昨收價,漲跌顯色,整齊排版,可以自己在STOCKS新增股票,就可以無限增加。
// @match        *://*/*
// @license      GPL-3.0 License
// @grant        GM_xmlhttpRequest
// @connect      mis.twse.com.tw
// ==/UserScript==

(function() {
    'use strict';

    const stocks = [
        { name: '加權指數', symbol: 'tse_t00.tw', id: 'price-index' },
        { name: '台積電', symbol: 'tse_2330.tw', id: 'price-tsmc' },
        { name: '聯發科', symbol: 'tse_2454.tw', id: 'price-mediatek' },
        { name: '鴻海', symbol: 'tse_2317.tw', id: 'price-foxconn' }
    ];

    const panel = document.createElement('div');
    Object.assign(panel.style, {
        position: 'fixed',
        top: '100px',
        left: '0px',
        background: '#fff',
        border: '1px solid #aaa',
        borderRadius: '8px',
        boxShadow: '2px 2px 8px rgba(0,0,0,0.3)',
        zIndex: 999999,
        width: '320px',
        fontFamily: 'monospace',
        whiteSpace: 'nowrap',
        padding: '5px'
    });

    const header = document.createElement('div');
    header.textContent = '台股資訊(TWSE API v1.7)';
    Object.assign(header.style, {
        background: '#333',
        color: '#fff',
        padding: '5px',
        cursor: 'move',
        textAlign: 'center',
        fontWeight: 'bold'
    });

    const minBtn = document.createElement('button');
    minBtn.textContent = '-';
    Object.assign(minBtn.style, {
        float: 'right',
        background: '#555',
        color: 'white',
        border: 'none',
        cursor: 'pointer',
        padding: '0 6px'
    });
    header.appendChild(minBtn);

    const refreshBtn = document.createElement('button');
    refreshBtn.textContent = '⟳';
    Object.assign(refreshBtn.style, {
        float: 'right',
        background: '#008CBA',
        color: 'white',
        border: 'none',
        cursor: 'pointer',
        padding: '0 6px',
        marginRight: '4px'
    });
    header.appendChild(refreshBtn);

    panel.appendChild(header);

    const body = document.createElement('div');
    Object.assign(body.style, {
        padding: '6px',
        fontSize: '13px',
        background: '#fff'
    });

    stocks.forEach(s => {
        const row = document.createElement('div');
        Object.assign(row.style, {
            display: 'grid',
            gridTemplateColumns: "1fr 1fr 1fr",
            alignItems: "center",
            marginBottom: '3px'
        });

        const label = document.createElement('span');
        label.style.textAlign = 'left';
        label.textContent = s.name;

        const price = document.createElement('span');
        price.id = s.id;
        price.style.textAlign = 'right';
        price.textContent = '載入中…';

        const closeLabel = document.createElement('span');
        closeLabel.style.textAlign = 'right';
        closeLabel.textContent = '';

        row.appendChild(label);
        row.appendChild(price);
        row.appendChild(closeLabel);
        body.appendChild(row);
    });

    const status = document.createElement('div');
    status.id = 'status';
    status.textContent = '最後更新:-';
    Object.assign(status.style, {
        color: '#666',
        fontSize: '11px',
        marginTop: '5px',
        textAlign: 'right'
    });
    body.appendChild(status);

    panel.appendChild(body);
    document.body.appendChild(panel);

    // 拖曳功能
    (function dragElement(el) {
        const header = el.querySelector('div');
        let pos1, pos2, pos3, pos4;
        header.onmousedown = function(e) {
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = close;
            document.onmousemove = drag;
        };
        function drag(e) {
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;
            el.style.top = (el.offsetTop - pos2) + "px";
            el.style.left = (el.offsetLeft - pos1) + "px";
        }
        function close() {
            document.onmouseup = null;
            document.onmousemove = null;
        }
    })(panel);

    // 收合功能
    minBtn.addEventListener('click', () => {
        if (body.style.display === 'none') {
            body.style.display = 'block';
            minBtn.textContent = '-';
        } else {
            body.style.display = 'none';
            minBtn.textContent = '+';
        }
    });

    refreshBtn.addEventListener('click', fetchQuotes);

    function fetchQuotes() {
        stocks.forEach(stock => {
            const url = `https://mis.twse.com.tw/stock/api/getStockInfo.jsp?ex_ch=${stock.symbol}&json=1&delay=0&_=${Date.now()}`;
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                headers: {
                    "User-Agent": "Mozilla/5.0",
                    "Accept": "*/*",
                    "Origin": "https://mis.twse.com.tw"
                },
                onload: function(res) {
                    try {
                        const data = JSON.parse(res.responseText);
                        const info = (data.msgArray && data.msgArray[0]) || null;
                        const el = document.getElementById(stock.id);
                        if (info && info.z && info.y) {
                            let price = parseFloat(info.z);
                            let closeYesterday = parseFloat(info.y);
                            let changePct = ((price - closeYesterday) / closeYesterday * 100).toFixed(2);
                            el.innerHTML = `<span style="color:${price >= closeYesterday ? 'red' : 'green'}">${price.toFixed(2)} (${changePct}%)</span> 昨收:${closeYesterday.toFixed(2)}`;
                        } else {
                            el.textContent = '無資料';
                            el.style.color = '#999';
                        }
                    } catch (err) {
                        console.error(stock.name + " 抓取失敗", err);
                        document.getElementById(stock.id).textContent = '錯誤';
                    }
                },
                onerror: function(err) {
                    console.error(stock.name + " 抓取失敗", err);
                    document.getElementById(stock.id).textContent = '錯誤';
                }
            });
        });
        status.textContent = '最後更新:' + new Date().toLocaleTimeString();
    }

    fetchQuotes();
    setInterval(fetchQuotes, 5 * 60 * 1000);
})();