| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>多股黄金分割智能看盘面板</title> |
| |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <meta name="apple-mobile-web-app-capable" content="yes"> |
| <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> |
| <meta name="apple-mobile-web-app-title" content="黄金分割"> |
| |
| <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script> |
| <script src="https://cdn.tailwindcss.com"></script> |
| </head> |
| <body class="h-screen w-screen overflow-hidden bg-gray-100 flex flex-col p-2 md:p-4 text-sm md:text-base"> |
| |
| <div class="flex items-center justify-between mb-2 md:mb-4 flex-shrink-0"> |
| <div class="flex items-center gap-2 md:gap-4"> |
| |
| <button onclick="toggleSidebar()" class="md:hidden text-gray-700 hover:text-blue-600 focus:outline-none p-1"> |
| <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path> |
| </svg> |
| </button> |
| <h1 class="text-xl md:text-2xl font-bold text-gray-800">📈 A股黄金分割 (云端版)</h1> |
| <div class="hidden md:flex items-center gap-2 px-3 py-1 bg-white rounded-full shadow-sm border border-gray-200"> |
| <span id="statusDot" class="w-3 h-3 rounded-full bg-red-500 animate-pulse"></span> |
| <span id="statusText" class="text-xs text-red-600 font-bold">等待后台启动...</span> |
| </div> |
| </div> |
| <div id="globalLoading" class="hidden text-blue-600 font-bold text-sm md:text-base">数据同步中...</div> |
| </div> |
|
|
| |
| <div class="flex flex-1 gap-2 md:gap-4 min-h-0 relative"> |
| |
| |
| <div id="sidebarOverlay" class="fixed inset-0 bg-black opacity-50 z-20 hidden md:hidden" onclick="toggleSidebar()"></div> |
|
|
| |
| <div id="sidebar" class="fixed inset-y-0 left-0 transform -translate-x-full md:relative md:translate-x-0 transition duration-200 ease-in-out z-30 w-64 md:w-64 bg-white border border-gray-200 rounded-none md:rounded shadow-2xl md:shadow p-4 flex flex-col flex-shrink-0 h-full md:h-auto"> |
| <div class="flex justify-between items-center border-b pb-2 mb-4"> |
| <h2 class="text-lg font-bold text-gray-700">自选股列表</h2> |
| <button onclick="toggleSidebar()" class="md:hidden text-gray-500 font-bold text-2xl leading-none">×</button> |
| </div> |
| |
| <div class="flex gap-2 mb-4 flex-shrink-0 relative"> |
| <div class="flex-1 min-w-0 relative"> |
| <input type="text" id="newSymbolInput" placeholder="输入代码/拼音" class="w-full px-2 py-1 bg-gray-50 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" autocomplete="off"> |
| <ul id="searchResults" class="absolute z-50 w-full bg-white border border-gray-200 shadow-lg max-h-60 overflow-y-auto hidden mt-1 rounded"></ul> |
| </div> |
| <button onclick="addStockFromInput()" class="flex-shrink-0 bg-blue-600 hover:bg-blue-700 text-white font-bold px-3 py-1 rounded">+</button> |
| </div> |
| |
| <ul id="stockList" class="flex-1 overflow-y-auto space-y-2 mb-4"> |
| |
| </ul> |
|
|
| <div class="flex flex-col gap-2 flex-shrink-0 border-t pt-4"> |
| <button onclick="saveState()" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded transition duration-150"> |
| 💾 保存所有设置 |
| </button> |
| <button onclick="refreshData()" class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded transition duration-150"> |
| 🔄 刷新所有数据 |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="flex-1 flex flex-col min-w-0 gap-2 md:gap-4 w-full"> |
| |
| |
| <div class="flex-1 bg-white border border-gray-200 rounded shadow flex flex-col min-h-0 relative"> |
| |
| <div class="flex gap-2 md:gap-4 p-2 md:p-4 border-b bg-gray-50 flex-shrink-0 items-end flex-wrap overflow-x-auto"> |
| <div> |
| <label class="block text-xs font-medium text-gray-600">前期高点</label> |
| <input type="number" id="highInput" class="w-20 md:w-24 mt-1 px-1 py-1 bg-white border border-gray-300 rounded shadow-sm text-xs md:text-sm"> |
| </div> |
| <div> |
| <label class="block text-xs font-medium text-gray-600">前期低点</label> |
| <input type="number" id="lowInput" class="w-20 md:w-24 mt-1 px-1 py-1 bg-white border border-gray-300 rounded shadow-sm text-xs md:text-sm"> |
| </div> |
| <div> |
| <label class="block text-xs font-medium text-gray-600">趋势方向</label> |
| <select id="trendInput" class="w-24 md:w-40 mt-1 px-1 py-1 bg-white border border-gray-300 rounded shadow-sm text-xs md:text-sm"> |
| <option value="up">上涨回撤</option> |
| <option value="down">下跌反弹</option> |
| </select> |
| </div> |
| <div class="flex-1 flex justify-end"> |
| <button onclick="calculateManual()" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-1 px-2 md:px-4 rounded shadow text-xs md:text-sm"> |
| 计算 |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="flex-1 relative min-h-[250px]"> |
| <div id="mainChart" class="absolute inset-0"></div> |
| </div> |
| </div> |
|
|
| |
| <div id="analysisPanelContainer" class="h-32 md:h-40 bg-blue-50 border border-blue-200 rounded shadow p-2 md:p-4 flex-shrink-0 flex flex-col"> |
| <h2 class="text-sm md:text-base font-bold text-blue-800 border-b border-blue-200 pb-1 mb-1 md:mb-2 flex-shrink-0">🤖 盘面动态轨迹分析</h2> |
| <div id="analysisPanel" class="flex-1 overflow-y-auto text-gray-700 leading-relaxed text-xs md:text-sm pr-2"> |
| 请先选择股票,并在主图中拖动右上角框选工具选取分析区间。 |
| </div> |
| </div> |
|
|
| </div> |
| </div> |
|
|
| <script> |
| |
| function toggleSidebar(forceShow) { |
| const sidebar = document.getElementById('sidebar'); |
| const overlay = document.getElementById('sidebarOverlay'); |
| if (forceShow === true || sidebar.classList.contains('-translate-x-full')) { |
| sidebar.classList.remove('-translate-x-full'); |
| overlay.classList.remove('hidden'); |
| } else { |
| sidebar.classList.add('-translate-x-full'); |
| overlay.classList.add('hidden'); |
| } |
| } |
| |
| let myChart = echarts.init(document.getElementById('mainChart')); |
| |
| let appState = { |
| current: null, |
| stocks: {} |
| }; |
| |
| window.addEventListener('resize', () => { |
| myChart.resize(); |
| }); |
| |
| |
| function calculateFibonacci(high, low, trend) { |
| const diff = high - low; |
| const ratios = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]; |
| let levels = {}; |
| ratios.forEach(ratio => { |
| if (trend === 'up') levels[ratio] = high - (diff * ratio); |
| else levels[ratio] = low + (diff * ratio); |
| }); |
| return levels; |
| } |
| |
| function generateAnalysis(stockData, brushEndIdx, levels, trend) { |
| if (stockData.length === 0) return ""; |
| const latestPrice = stockData[stockData.length - 1].close; |
| const postWaveData = stockData.slice(brushEndIdx + 1); |
| let analysis = ""; |
| |
| if (trend === 'up') { |
| let min_low = latestPrice; |
| if (postWaveData.length > 0) min_low = Math.min(...postWaveData.map(d => parseFloat(d.low))); |
| const bounce = ((latestPrice - min_low) / min_low) * 100; |
| |
| let depthStr = ""; |
| if (min_low > levels['0.382']) depthStr = "最大回撤极浅(未破0.382),强势洗盘。"; |
| else if (min_low > levels['0.618']) depthStr = `最大回撤触及 ${min_low < levels['0.5'] ? '0.618 附近' : '0.5 半山腰'}。`; |
| else depthStr = "深度回调,跌破0.618防线,形态遭破坏。"; |
| |
| let currentStr = bounce > 3 ? `现已企稳,较底部回升 <span class="text-red-600 font-bold">${bounce.toFixed(2)}%</span>。` : `最新价距底部无明显反弹,弱势筑底。`; |
| analysis = depthStr + " " + currentStr; |
| } else { |
| let max_high = latestPrice; |
| if (postWaveData.length > 0) max_high = Math.max(...postWaveData.map(d => parseFloat(d.high))); |
| const drop = ((max_high - latestPrice) / max_high) * 100; |
| |
| let heightStr = ""; |
| if (max_high < levels['0.382']) heightStr = "反弹极微弱(未触及0.382),空头压制明显。"; |
| else if (max_high < levels['0.618']) heightStr = `中级反抽,最高触及 ${max_high > levels['0.5'] ? '0.618 强阻' : '0.5 阻力'}。`; |
| else heightStr = "反弹强势,曾上攻突破0.618。"; |
| |
| let currentStr = drop > 3 ? `现遇阻回落,较最高点下跌 <span class="text-green-600 font-bold">${drop.toFixed(2)}%</span>。` : `最新价维持高位,有向上突破迹象。`; |
| analysis = heightStr + " " + currentStr; |
| } |
| return analysis; |
| } |
| |
| |
| function drawChart(symbol, isNewLoad = false) { |
| const stockState = appState.stocks[symbol]; |
| if (!stockState || stockState.data.length === 0) return; |
| |
| const high = parseFloat(document.getElementById('highInput').value) || stockState.high; |
| const low = parseFloat(document.getElementById('lowInput').value) || stockState.low; |
| const trend = document.getElementById('trendInput').value || stockState.trend; |
| |
| const dates = stockState.data.map(item => item.date); |
| const values = stockState.data.map(item => [item.open, item.close, item.low, item.high]); |
| |
| let markLineData = []; |
| if (high && low) { |
| const fibLevels = calculateFibonacci(high, low, trend); |
| markLineData = Object.keys(fibLevels).map(ratio => { |
| let color = ratio === '0.618' || ratio === '0.382' ? '#e74c3c' : '#34495e'; |
| return { |
| yAxis: fibLevels[ratio], |
| label: { formatter: `{b}:{c}`, position: 'end', color: color, fontWeight: 'bold' }, |
| lineStyle: { type: 'dashed', color: color, width: 2 }, |
| name: `Fib ${ratio}` |
| }; |
| }); |
| |
| const analysisHtml = generateAnalysis(stockState.data, stockState.lastBrushEndIndex, fibLevels, trend); |
| stockState.analysisHtml = analysisHtml; |
| document.getElementById('analysisPanel').innerHTML = analysisHtml; |
| } |
| |
| let currentStart = stockState.zoomStart || 0; |
| let currentEnd = stockState.zoomEnd || 100; |
| const totalDays = stockState.data.length; |
| |
| if (isNewLoad && totalDays > 0) { |
| currentStart = Math.max(0, ((totalDays - 125) / totalDays) * 100); |
| } else if (!isNewLoad) { |
| const currentOption = myChart.getOption(); |
| if (currentOption && currentOption.dataZoom && currentOption.dataZoom.length > 0) { |
| currentStart = currentOption.dataZoom[0].start; |
| currentEnd = currentOption.dataZoom[0].end; |
| } |
| } |
| |
| stockState.zoomStart = currentStart; |
| stockState.zoomEnd = currentEnd; |
| |
| const stockName = stockState.name ? ` ${stockState.name}` : ''; |
| const option = { |
| title: { text: `[${symbol}${stockName}]`, left: 0, textStyle: { fontSize: window.innerWidth < 768 ? 14 : 18 } }, |
| tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } }, |
| toolbox: { feature: { brush: { type: ['lineX', 'clear'] } } }, |
| brush: { xAxisIndex: 'all', brushLink: 'all', outOfBrush: { colorAlpha: 0.3 } }, |
| grid: { left: window.innerWidth < 768 ? '10%' : '3%', right: window.innerWidth < 768 ? '15%' : '8%', bottom: '15%', top: '15%' }, |
| xAxis: { type: 'category', data: dates, scale: true, boundaryGap: false }, |
| yAxis: { scale: true, splitArea: { show: true } }, |
| dataZoom: [ |
| { type: 'inside', start: currentStart, end: currentEnd }, |
| { show: true, type: 'slider', top: '90%', start: currentStart, end: currentEnd } |
| ], |
| series: [{ |
| name: '日K', type: 'candlestick', data: values, |
| itemStyle: { color: '#ef232a', color0: '#14b143', borderColor: '#ef232a', borderColor0: '#14b143' }, |
| markLine: { symbol: ['none', 'none'], data: markLineData } |
| }] |
| }; |
| |
| myChart.setOption(option); |
| |
| if (stockState.brushRange) { |
| setTimeout(() => { |
| myChart.dispatchAction({ |
| type: 'brush', |
| areas: [{ brushType: 'lineX', coordRange: stockState.brushRange, xAxisIndex: 0 }] |
| }); |
| }, 100); |
| } |
| } |
| |
| |
| myChart.on('brushEnd', function(params) { |
| if (!appState.current || params.areas.length === 0) return; |
| const stockState = appState.stocks[appState.current]; |
| const range = params.areas[0].coordRange; |
| stockState.brushRange = range; |
| |
| const startIndex = Math.max(0, Math.floor(range[0])); |
| const endIndex = Math.min(stockState.data.length - 1, Math.ceil(range[1])); |
| stockState.lastBrushEndIndex = endIndex; |
| |
| const slicedData = stockState.data.slice(startIndex, endIndex + 1); |
| if (slicedData.length > 0) { |
| let maxHigh = -Infinity, minLow = Infinity; |
| let maxIdx = -1, minIdx = -1; |
| slicedData.forEach((d, i) => { |
| const h = parseFloat(d.high), l = parseFloat(d.low); |
| if (h > maxHigh) { maxHigh = h; maxIdx = i; } |
| if (l < minLow) { minLow = l; minIdx = i; } |
| }); |
| stockState.high = maxHigh; |
| stockState.low = minLow; |
| stockState.trend = (minIdx < maxIdx) ? 'up' : 'down'; |
| updateUIFromState(appState.current); |
| } |
| }); |
| |
| myChart.on('datazoom', function () { |
| if(!appState.current) return; |
| const option = myChart.getOption(); |
| appState.stocks[appState.current].zoomStart = option.dataZoom[0].start; |
| appState.stocks[appState.current].zoomEnd = option.dataZoom[0].end; |
| }); |
| |
| function calculateManual() { |
| if (!appState.current) return; |
| const stockState = appState.stocks[appState.current]; |
| stockState.high = parseFloat(document.getElementById('highInput').value); |
| stockState.low = parseFloat(document.getElementById('lowInput').value); |
| stockState.trend = document.getElementById('trendInput').value; |
| drawChart(appState.current, false); |
| } |
| |
| |
| function updateUIFromState(symbol) { |
| const stockState = appState.stocks[symbol]; |
| if(!stockState) return; |
| if(stockState.high) document.getElementById('highInput').value = stockState.high.toFixed(2); |
| if(stockState.low) document.getElementById('lowInput').value = stockState.low.toFixed(2); |
| if(stockState.trend) document.getElementById('trendInput').value = stockState.trend; |
| document.getElementById('analysisPanel').innerHTML = stockState.analysisHtml || "请在主图中拖动框选工具选取分析区间。"; |
| } |
| |
| function renderStockList() { |
| const ul = document.getElementById('stockList'); |
| ul.innerHTML = ''; |
| Object.keys(appState.stocks).forEach(sym => { |
| const st = appState.stocks[sym]; |
| const displayName = st.name ? `${sym} ${st.name}` : sym; |
| const li = document.createElement('li'); |
| li.className = `flex justify-between items-center p-2 rounded cursor-pointer border text-sm ${sym === appState.current ? 'bg-blue-100 border-blue-400 font-bold' : 'bg-gray-50 border-gray-200 hover:bg-gray-100'}`; |
| |
| const textSpan = document.createElement('span'); |
| textSpan.className = 'flex-1 truncate'; |
| textSpan.innerText = displayName; |
| textSpan.onclick = () => switchStock(sym); |
| |
| const removeBtn = document.createElement('button'); |
| removeBtn.className = 'ml-2 text-red-400 hover:text-red-600 font-bold text-lg leading-none p-1'; |
| removeBtn.innerText = '×'; |
| removeBtn.onclick = (e) => { e.stopPropagation(); removeStock(sym); }; |
| |
| li.appendChild(textSpan); |
| li.appendChild(removeBtn); |
| ul.appendChild(li); |
| }); |
| } |
| |
| function removeStock(symbol) { |
| delete appState.stocks[symbol]; |
| if (appState.current === symbol) { |
| const remaining = Object.keys(appState.stocks); |
| if (remaining.length > 0) switchStock(remaining[0]); |
| else { |
| appState.current = null; |
| myChart.clear(); |
| document.getElementById('analysisPanel').innerHTML = ""; |
| document.getElementById('highInput').value = ''; |
| document.getElementById('lowInput').value = ''; |
| renderStockList(); |
| } |
| } else { |
| renderStockList(); |
| } |
| } |
| |
| function switchStock(symbol) { |
| appState.current = symbol; |
| renderStockList(); |
| updateUIFromState(symbol); |
| myChart.clear(); |
| drawChart(symbol, false); |
| setTimeout(() => myChart.resize(), 10); |
| |
| |
| if (window.innerWidth < 768) { |
| toggleSidebar(false); |
| } |
| } |
| |
| async function fetchStockDataAPI(symbol) { |
| try { |
| |
| const response = await fetch(`/api/stock?symbol=${symbol}`); |
| if (!response.ok) throw new Error("网络错误"); |
| return await response.json(); |
| } catch (error) { |
| alert(`无法拉取 ${symbol} 数据`); |
| return null; |
| } |
| } |
| |
| async function addStockFromInput() { |
| const symbol = document.getElementById('newSymbolInput').value.trim(); |
| if (!symbol) return; |
| if (appState.stocks[symbol]) { |
| switchStock(symbol); |
| return; |
| } |
| |
| document.getElementById('globalLoading').classList.remove('hidden'); |
| const data = await fetchStockDataAPI(symbol); |
| document.getElementById('globalLoading').classList.add('hidden'); |
| |
| if (data && data.data && data.data.length > 0) { |
| appState.stocks[symbol] = { |
| name: data.name || "", data: data.data, |
| high: null, low: null, trend: 'up', zoomStart: null, zoomEnd: null, |
| brushRange: null, lastBrushEndIndex: data.data.length - 1, analysisHtml: "" |
| }; |
| let mx = -Infinity, mn = Infinity; |
| data.data.forEach(d => { if(d.high > mx) mx=d.high; if(d.low < mn) mn=d.low; }); |
| appState.stocks[symbol].high = mx; appState.stocks[symbol].low = mn; |
| |
| document.getElementById('newSymbolInput').value = ''; |
| appState.current = symbol; |
| renderStockList(); |
| updateUIFromState(symbol); |
| drawChart(symbol, true); |
| if (window.innerWidth < 768) toggleSidebar(false); |
| } |
| } |
| |
| function saveState() { |
| let storableState = { current: appState.current, stocks: {} }; |
| for (let sym in appState.stocks) { |
| const st = appState.stocks[sym]; |
| storableState.stocks[sym] = { |
| name: st.name, high: st.high, low: st.low, trend: st.trend, |
| zoomStart: st.zoomStart, zoomEnd: st.zoomEnd, |
| brushRange: st.brushRange, lastBrushEndIndex: st.lastBrushEndIndex |
| }; |
| } |
| localStorage.setItem('stockGoldRuleV2', JSON.stringify(storableState)); |
| alert('设置已成功保存到浏览器本地缓存!'); |
| } |
| |
| async function loadState() { |
| const saved = localStorage.getItem('stockGoldRuleV2'); |
| if (!saved) return; |
| const parsed = JSON.parse(saved); |
| document.getElementById('globalLoading').classList.remove('hidden'); |
| for (let sym in parsed.stocks) { |
| const st = parsed.stocks[sym]; |
| const res = await fetchStockDataAPI(sym); |
| if (res && res.data) { |
| appState.stocks[sym] = { |
| name: res.name || st.name || "", data: res.data, |
| high: st.high, low: st.low, trend: st.trend, |
| zoomStart: st.zoomStart, zoomEnd: st.zoomEnd, |
| brushRange: st.brushRange, lastBrushEndIndex: st.lastBrushEndIndex, analysisHtml: "" |
| }; |
| } |
| } |
| document.getElementById('globalLoading').classList.add('hidden'); |
| if (parsed.current && appState.stocks[parsed.current]) switchStock(parsed.current); |
| else renderStockList(); |
| } |
| |
| async function refreshData() { |
| document.getElementById('globalLoading').classList.remove('hidden'); |
| for (let sym in appState.stocks) { |
| const res = await fetchStockDataAPI(sym); |
| if (res && res.data) { |
| appState.stocks[sym].data = res.data; |
| appState.stocks[sym].name = res.name || appState.stocks[sym].name || ""; |
| } |
| } |
| document.getElementById('globalLoading').classList.add('hidden'); |
| if (appState.current) switchStock(appState.current); |
| alert('全量数据已刷新至最新交易日!'); |
| } |
| |
| let isServerReady = false; |
| async function checkServerStatus() { |
| try { |
| const controller = new AbortController(); |
| const timeoutId = setTimeout(() => controller.abort(), 2000); |
| const res = await fetch("/api/health", { signal: controller.signal }); |
| clearTimeout(timeoutId); |
| if (res.ok) { |
| if (!isServerReady) { |
| isServerReady = true; |
| document.getElementById('statusDot').className = "w-3 h-3 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.8)]"; |
| document.getElementById('statusText').className = "text-xs md:text-sm text-green-600 font-bold"; |
| document.getElementById('statusText').innerText = "服务已连接"; |
| loadState(); |
| } |
| } else throw new Error("Not OK"); |
| } catch (e) { |
| if (isServerReady || !isServerReady) { |
| isServerReady = false; |
| document.getElementById('statusDot').className = "w-3 h-3 rounded-full bg-red-500 animate-pulse"; |
| document.getElementById('statusText').className = "text-xs md:text-sm text-red-600 font-bold"; |
| document.getElementById('statusText').innerText = "服务连接中..."; |
| } |
| } |
| } |
| |
| |
| let searchTimeout = null; |
| const searchInput = document.getElementById('newSymbolInput'); |
| const searchResults = document.getElementById('searchResults'); |
| searchInput.addEventListener('input', (e) => { |
| const query = e.target.value.trim(); |
| if (!query) { searchResults.classList.add('hidden'); return; } |
| if (searchTimeout) clearTimeout(searchTimeout); |
| searchTimeout = setTimeout(async () => { |
| try { |
| const res = await fetch("/api/search?q=" + encodeURIComponent(query)); |
| if (!res.ok) return; |
| const data = await res.json(); |
| if (data && data.length > 0) { |
| searchResults.innerHTML = ''; |
| data.forEach(item => { |
| const li = document.createElement('li'); |
| li.className = 'px-3 py-2 cursor-pointer hover:bg-blue-50 border-b border-gray-100 text-sm text-gray-700'; |
| li.innerHTML = `<strong>${item.code}</strong> ${item.name} <span class="text-xs text-gray-400">(${item.pinyin})</span>`; |
| li.onclick = () => { |
| searchInput.value = item.code; |
| searchResults.classList.add('hidden'); |
| addStockFromInput(); |
| }; |
| searchResults.appendChild(li); |
| }); |
| searchResults.classList.remove('hidden'); |
| } else { |
| searchResults.innerHTML = '<li class="px-3 py-2 text-sm text-gray-500">无结果</li>'; |
| searchResults.classList.remove('hidden'); |
| } |
| } catch (error) {} |
| }, 300); |
| }); |
| |
| document.addEventListener('click', (e) => { |
| if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) { |
| searchResults.classList.add('hidden'); |
| } |
| }); |
| |
| window.onload = () => { |
| checkServerStatus(); |
| setInterval(checkServerStatus, 2000); |
| }; |
| </script> |
| </body> |
| </html> |
|
|