PageSourceSearch

https://xincai.com/static/public/utils.js

js xincai.com collected 2026-10-04 02:49:20 UTC 13,832 bytes, 399 lines download raw bytes

1class XCTab {
2    constructor(onTabChange) {
3        this.tabs = new Map();
4        this.onTabChange = onTabChange;
5    }
6
7    add(tabId, contentId) {
8        const tabElement = document.getElementById(tabId);
9        const contentElement = document.getElementById(contentId);
10
11        if (!tabElement || !contentElement) {
12            console.error(`Tab or content element not found for ids: ${tabId} and ${contentId}`);
13            return;
14        }
15
16        this.tabs.set(tabElement, contentElement);
17
18        tabElement.addEventListener('click', () => {
19            this.showTab(tabElement);
20        });
21    }
22
23    selectTab(tabId) {
24        const tabElement = document.getElementById(tabId);
25
26        if (!tabElement) {
27            console.error(`Tab element not found for id: ${tabId}`);
28            return;
29        }
30
31        this.showTab(tabElement);
32    }
33
34    showTab(selectedTab) {
35        for (const [tabElement, contentElement] of this.tabs) {
36            if (tabElement === selectedTab) {
37                tabElement.classList.add('xc-tab-active');
38                contentElement.style.display = 'block';
39            } else {
40                tabElement.classList.remove('xc-tab-active');
41                contentElement.style.display = 'none';
42            }
43        }
44
45        if (typeof this.onTabChange === 'function') {
46            this.onTabChange(selectedTab);
47        }
48    }
49}
50
51class Utils {
52    constructor() {
53        throw new Error("Utils class cannot be instantiated.");
54    }
55
56    static getRedOrGreen(changeRate) {
57        var changeRateNum = parseFloat(changeRate)
58        if (changeRateNum > 0) {
59            return '#f00'
60        } else if (changeRateNum < 0) {
61            return '#008000'
62        } else {
63            return '#666'
64        }
65    }
66
67    static fixed(changeRate) {
68        var changeRateNum = parseFloat(changeRate)
69        if (changeRateNum > 0) {
70            return '+' + changeRateNum.toFixed(2) + '%'
71        } else if (changeRateNum < 0) {
72            return changeRateNum.toFixed(2) + '%'
73        } else {
74            return '0.00%'
75        }
76    }
77
78    static parseStockData(code, hqDataStr, fixed = 2) {
79        const hqDataArray = hqDataStr.split(',');
80
81        const open = parseFloat(hqDataArray[1]).toFixed(fixed);
82        const close = parseFloat(hqDataArray[2]).toFixed(fixed);
83        const now = parseFloat(hqDataArray[3]).toFixed(fixed);
84        const high = parseFloat(hqDataArray[4]).toFixed(fixed);
85        const low = parseFloat(hqDataArray[5]).toFixed(fixed);
86
87        // @ts-ignore
88        const change = (now - close).toFixed(fixed); // 涨跌额
89        // @ts-ignore
90        const changeRate = (((now - close) / close) * 100).toFixed(fixed); // 涨跌幅
91        const changeRateStr = parseFloat(changeRate) > 0 ?
92            '+' + changeRate + '%' :
93            changeRate + '%';
94
95        return {
96            code,
97            name: hqDataArray[0],
98            open,
99            close,
100            now,
101            high,
102            low,
103            total: parseInt(hqDataArray[8]),
104            volume: parseFloat(hqDataArray[9]).toFixed(fixed),
105            date: hqDataArray[30],
106            time: hqDataArray[31],
107            status: hqDataArray[32],
108            change,
109            changeRate,
110            changeRateStr,
111        };
112    }
113
114    /**
115     * Send a JSONP request to the specified URL with optional parameters.
116     * @param {string} url - The URL to send the request to.
117     * @param {Object} [params] - Optional parameters to include in the request.
118     * @returns {Promise} - A Promise that resolves with the response data or rejects with an error.
119     */
120    static jsonp(url, params) {
121        return new Promise((resolve, reject) => {
122            const callbackName = 'jsonp_callback_' + Math.floor(Math.random() * 100000);
123            window[callbackName] = (data) => {
124                delete window[callbackName];
125                const script = document.getElementById(callbackName);
126                script && script.parentNode && script.parentNode.removeChild(script);
127                resolve(data);
128            };
129            const script = document.createElement('script');
130            script.src = url + (url.includes('?') ? '&' : '?') + 'callback=' + callbackName + (params ? ('&' + Object.keys(params).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key])).join('&')) : '');
131            script.id = callbackName;
132            script.onerror = (err) => {
133                delete window[callbackName];
134                const script = document.getElementById(callbackName);
135                script && script.parentNode && script.parentNode.removeChild(script);
136                reject(err);
137            };
138            document.getElementsByTagName('head')[0].appendChild(script);
139        });
140    }
141
142    /**
143     * Loads a script dynamically.
144     * @param {string} url - The URL of the script to load.
145     * @param {boolean} [removeAfterLoad=true] - Whether to remove the script element after loading.
146     * @returns {Promise<string>} A promise that resolves with the URL of the loaded script.
147     */
148    static loadScript(url, removeAfterLoad = true) {
149        return new Promise((resolve, reject) => {
150            const script = document.createElement('script');
151            script.src = url;
152            script.onload = () => {
153                resolve(url);
154                if (removeAfterLoad) {
155                    document.head.removeChild(script);
156                }
157            };
158            script.onerror = (e) => {
159                reject(e);
160                if (removeAfterLoad) {
161                    document.head.removeChild(script);
162                }
163            };
164            document.head.appendChild(script);
165        });
166    }
167
168    static requestStock(hqList = ['sh513330']) {
169        return new Promise((resolve, reject) => {
170            Utils.loadScript(`https://hq.sinajs.cn/rn=1679905875258&list=${hqList.join(',')}`).then(() => {
171                resolve(hqList.map(e => Utils.parseStockData(e, window[`hq_str_${e}`])));
172            }).catch(error => {
173                reject(error);
174            })
175        });
176    }
177
178    static debounce(func, delay) {
179        let timer; return function (...args) {
180            const context = this; clearTimeout(timer);
181            timer = setTimeout(() => {
182                func.apply(context, args);
183            }, delay);
184        };
185    }
186
187    static isOrgArticle(url) {
188        const docIDReg = /-i(\w{7}\d{7}).shtml/gi;
189        return docIDReg.test(url);
190    }
191
192    static getDocIDFromOrgArticle(url) {
193        const docIDReg = /-i(\w{7}\d{7}).shtml/gi;
194        return url.match(docIDReg)[0].replace('-i', '').replace('.shtml', '');
195    }
196
197    static getXincaiArticleUrl(url) {
198        const docIDReg = /-i(\w{7}\d{7}).shtml/gi;
199        if (docIDReg.test(url)) {
200            return `/article/${url.match(docIDReg)[0].replace('-i', '').replace('.shtml', '')}`;
201        } else {
202            return null;
203        }
204    }
205
206    static getVisitedStock() {
207        const VISITED_STORAGE_KEY = 'xincai_visited_stock';
208        const storage = localStorage.getItem(VISITED_STORAGE_KEY);
209        if (storage) {
210            return storage.split(',');
211        } else {
212            return null;
213        }
214    }
215
216    static isInVisitedStock(code) {
217        if (!/\w{2}\d{6}/gi.test(code)) {
218            throw new Error('code is not valid: ' + code);
219        }
220        const VISITED_STORAGE_KEY = 'xincai_visited_stock';
221        const storage = localStorage.getItem(VISITED_STORAGE_KEY);
222        if (storage) {
223            return storage.split(',').includes(code);
224        }
225        return false;
226    }
227
228    static addVisitedStock(code) {
229        if (!/\w{2}\d{6}/gi.test(code)) {
230            throw new Error('code is not valid: ' + code);
231        }
232        if (!Utils.isInVisitedStock(code)) {
233            const VISITED_STORAGE_KEY = 'xincai_visited_stock';
234            const storage = localStorage.getItem(VISITED_STORAGE_KEY);
235            if (storage) {
236                const newStorage = code + ',' + storage;
237                localStorage.setItem(VISITED_STORAGE_KEY, newStorage);
238            } else {
239                localStorage.setItem(VISITED_STORAGE_KEY, code);
240            }
241        }
242    }
243}
244
245// 统一行情数据管理
246class StockDataController {
247    constructor() {
248        throw new Error("StockDataController class cannot be instantiated.");
249    }
250
251    static codeList = [];
252
253    static #listeners = {
254        // 'sh000300': [],
255    };
256
257    static #interval = null;
258
259    static #requestDebounce = Utils.debounce(codeList => {
260        return Utils.requestStock(codeList).then(dataList => {
261            // console.log(dataList);
262            for (let data of dataList) {
263                if (data.code in StockDataController.#listeners) {
264                    for (let resolve of StockDataController.#listeners[data.code]) {
265                        resolve(data);
266                    }
267                }
268            }
269        });
270    }, 100);
271
272    // 增加行情数据更新观察者
273    static addCodeListener(codeStr, resolve) {
274        // 加入回调列表
275        if (!StockDataController.#listeners[codeStr]) {
276            StockDataController.#listeners[codeStr] = [resolve];
277        } else {
278            StockDataController.#listeners[codeStr].push(resolve);
279        }
280        // 加入合并请求列表
281        StockDataController.codeList.push(codeStr);
282        // 尝试立即请求一次
283        StockDataController.#requestDebounce(StockDataController.codeList);
284        if (!StockDataController.#interval) {
285            // 定期请求数据
286            // @ts-ignore
287            StockDataController.#interval = setInterval(() => {
288                StockDataController.#requestDebounce(StockDataController.codeList);
289            }, 1000 * 5);
290        }
291    }
292
293    // 批量增加行情数据更新观察者
294    static addCodeListListener(codeList, resolve) {
295        for (let codeStr of codeList) {
296            StockDataController.addCodeListener(codeStr, resolve);
297        }
298    }
299
300}
301
302
303
304class StockCNList {
305    constructor({ wrapElement, hqCodelist }) {
306        this.hqCodelist = hqCodelist;
307        this.wrapElement = wrapElement;
308        this.wrapElement.classList.add('stock-cn-list');
309        this.wrapElement.innerHTML = `
310            <style>
311                .sf-loading-wrap {
312                    min-height: 200px;
313                    height: 100%;
314                    display: flex;
315                    justify-content: center;
316                    align-items: center;
317                }
318
319                .sf-loading {
320                    position: relative;
321                    z-index: 100000;
322                    padding: 0;
323                    margin: 0;
324                    width: 50px;
325                    height: 50px;
326                    border-radius: 999px;
327                    border: 4px solid #508CEE;
328                    border-top-color: transparent;
329                    border-left-color: transparent;
330                    animation: sf-rotate360 1s infinite linear;
331                }
332
333                .sf-loading::before {
334                    content: '';
335                    position: absolute;
336                    z-index: 100001;
337                    left: -4px;
338                    top: -4px;
339                    width: 50px;
340                    height: 50px;
341                    border-radius: 999px;
342                    border: 4px solid #508CEE;
343                    border-top-color: transparent;
344                    border-left-color: transparent;
345                    transform: rotate(120deg);
346                }
347
348                .sf-loading::after {
349                    content: '';
350                    position: absolute;
351                    left: 26px;
352                    top: -6px;
353                    z-index: 100002;
354                    border-radius: 999px;
355                    border: 6px solid #508CEE;
356                }
357
358                @keyframes sf-rotate360 {
359                    0% {
360                        transform: rotate(0deg) scale(0.5);
361                        transform-origin: center;
362                    }
363
364                    100% {
365                        transform: rotate(360deg) scale(0.5);
366                        transform-origin: center;
367                    }
368                }
369            </style>
370            <div class="sf-loading-wrap"><span class="sf-loading"></span></div>
371        `;
372        this.startRequestStock();
373    }
374
375    startRequestStock() {
376        /* Utils.requestStock(this.hqCodelist).then(dataList => {
377            // console.log(dataList)
378            let htmlStr = dataList.map(data => {
379                return '<p class="stock-item"><span class="stock-name">' + data.name + '</span><span class="stock-price" style="color: ' + Utils.getRedOrGreen(data.change) + '">' + data.now + '</span><span class="stock-change" style="color: ' + Utils.getRedOrGreen(data.change) + '">' + data.changeRateStr + '</span></p>'
380            }).join('');
381            this.wrapElement.innerHTML = htmlStr;
382        });
383
384        setTimeout(() => {
385            this.startRequestStock();
386        }, 10 * 1000); */
387        let htmlStr = '';
388        let count = 0;
389        StockDataController.addCodeListListener(this.hqCodelist, (data) => {
390            htmlStr += '<p class="stock-item"><span class="stock-name">' + data.name + '</span><span class="stock-price" style="color: ' + Utils.getRedOrGreen(data.change) + '">' + data.now + '</span><span class="stock-change" style="color: ' + Utils.getRedOrGreen(data.change) + '">' + data.changeRateStr + '</span></p>';
391            count++;
392            if (count === this.hqCodelist.length) {
393                this.wrapElement.innerHTML = htmlStr;
394                count = 0;
395                htmlStr = '';
396            }
397        });
398    }
399}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.