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.