|
|
<!DOCTYPE html> |
|
|
<html lang="zh-TW"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>股票殖利率查詢 (HTML5 版)</title> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<style> |
|
|
body { |
|
|
font-family: 'Inter', 'Noto Sans TC', sans-serif; |
|
|
} |
|
|
.clickable-tag { |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
.clickable-tag:hover { |
|
|
opacity: 0.7; |
|
|
transform: translateY(-1px); |
|
|
} |
|
|
|
|
|
@media (max-width: 640px) { |
|
|
table { |
|
|
font-size: 0.8rem; |
|
|
} |
|
|
.px-4 { |
|
|
padding-left: 0.75rem; |
|
|
padding-right: 0.75rem; |
|
|
} |
|
|
.py-2 { |
|
|
padding-top: 0.5rem; |
|
|
padding-bottom: 0.5rem; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-50 text-gray-800"> |
|
|
|
|
|
<div class="container mx-auto p-4 md:p-8 max-w-full"> |
|
|
<header class="mb-6"> |
|
|
<h1 class="text-3xl font-bold text-gray-900">股票殖利率查詢 (HTML5 版)</h1> |
|
|
<p class="text-gray-600 mt-1">此工具會即時爬取 Wespai 資料並在您的瀏覽器中進行過濾。</p> |
|
|
</header> |
|
|
|
|
|
|
|
|
<div class="bg-white p-4 rounded-lg shadow-sm mb-6"> |
|
|
<div class="flex flex-col sm:flex-row gap-2"> |
|
|
<input type="text" id="search-input" class="flex-grow p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none" placeholder="輸入股票代號或公司名稱 (例如: 2330 或 台積)"> |
|
|
<button id="search-button" class="bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg shadow hover:bg-blue-700 transition duration-200"> |
|
|
搜尋 |
|
|
</button> |
|
|
</div> |
|
|
<div id="history-container" class="mt-4 text-sm text-gray-600 flex flex-wrap gap-2 items-center"> |
|
|
<strong class="mr-2">搜尋記錄:</strong> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="status-message" class="my-4 p-3 bg-blue-100 text-blue-800 rounded-lg text-center font-medium"> |
|
|
正在初始化... |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="overflow-x-auto bg-white shadow-md rounded-lg"> |
|
|
<table class="min-w-full divide-y divide-gray-200"> |
|
|
<thead class="bg-gray-100"> |
|
|
<tr> |
|
|
|
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">代號</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">公司</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">配息</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">除息日</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">配股</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">除權日</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">股價</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">現金殖利率</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">殖利率</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">發息日</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">配息率</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">董監持股</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">3年平均股利</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">6年平均股利</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">10年平均股利</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">10年股利次數</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">1QEPS</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">2QEPS</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">3QEPS</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">今年累積EPS</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody id="results-table-body" class="bg-white divide-y divide-gray-200"> |
|
|
|
|
|
<tr> |
|
|
<td colspan="20" class="px-4 py-6 text-center text-gray-500">請稍候,正在載入資料...</td> |
|
|
</tr> |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<script type="module"> |
|
|
|
|
|
|
|
|
|
|
|
const WESPAI_URL = 'https://stock.wespai.com/rate114'; |
|
|
const PROXY_URL = 'https://api.allorigins.win/raw?url='; |
|
|
const HISTORY_KEY = 'stockSearchHistory'; |
|
|
const MAX_HISTORY = 10; |
|
|
|
|
|
|
|
|
const TARGET_COLUMNS = [ |
|
|
"代號", "公司", "配息", "除息日", "配股", "除權日", "股價", |
|
|
"現金殖利率", "殖利率", "發息日", "配息率", "董監持股", |
|
|
"3年平均股利", "6年平均股利", "10年平均股利", "10年股利次數", |
|
|
"1QEPS", "2QEPS", "3QEPS", "今年累積EPS", |
|
|
]; |
|
|
|
|
|
let allStockData = []; |
|
|
let searchHistory = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const searchInput = document.getElementById('search-input'); |
|
|
const searchButton = document.getElementById('search-button'); |
|
|
const statusMessage = document.getElementById('status-message'); |
|
|
const tableBody = document.getElementById('results-table-body'); |
|
|
const historyContainer = document.getElementById('history-container'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchData() { |
|
|
statusMessage.textContent = '正在從 Wespai 載入即時資料,請稍候...'; |
|
|
statusMessage.className = 'my-4 p-3 bg-blue-100 text-blue-800 rounded-lg text-center font-medium'; |
|
|
tableBody.innerHTML = `<tr><td colspan="${TARGET_COLUMNS.length}" class="px-4 py-6 text-center text-gray-500">正在載入...</td></tr>`; |
|
|
|
|
|
try { |
|
|
const response = await fetch(PROXY_URL + encodeURIComponent(WESPAI_URL)); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP 錯誤! 狀態: ${response.status}`); |
|
|
} |
|
|
|
|
|
const htmlString = await response.text(); |
|
|
allStockData = parseHTMLTable(htmlString); |
|
|
|
|
|
if (allStockData.length === 0) { |
|
|
statusMessage.textContent = '成功載入頁面,但無法解析到表格資料。'; |
|
|
statusMessage.className = 'my-4 p-3 bg-yellow-100 text-yellow-800 rounded-lg text-center font-medium'; |
|
|
tableBody.innerHTML = `<tr><td colspan="${TARGET_COLUMNS.length}" class="px-4 py-6 text-center text-gray-500">無法解析資料</td></tr>`; |
|
|
} else { |
|
|
statusMessage.textContent = `資料載入完成。共 ${allStockData.length} 筆。請輸入關鍵字搜尋。`; |
|
|
statusMessage.className = 'my-4 p-3 bg-green-100 text-green-800 rounded-lg text-center font-medium'; |
|
|
renderTable(allStockData); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('抓取資料失敗:', error); |
|
|
statusMessage.textContent = '資料載入失敗。可能是 CORS 代理或目標網站暫時無法訪問。'; |
|
|
statusMessage.className = 'my-4 p-3 bg-red-100 text-red-800 rounded-lg text-center font-medium'; |
|
|
tableBody.innerHTML = `<tr><td colspan="${TARGET_COLUMNS.length}" class="px-4 py-6 text-center text-red-500">載入失敗</td></tr>`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function parseHTMLTable(htmlString) { |
|
|
try { |
|
|
const parser = new DOMParser(); |
|
|
const doc = parser.parseFromString(htmlString, 'text/html'); |
|
|
const table = doc.querySelector('table.table-hover'); |
|
|
|
|
|
if (!table) { |
|
|
console.error('在 HTML 中找不到 table.table-hover'); |
|
|
return []; |
|
|
} |
|
|
|
|
|
|
|
|
const actualHeaders = [...table.querySelectorAll('thead th')].map(th => th.textContent.trim()); |
|
|
|
|
|
|
|
|
const colIndices = {}; |
|
|
TARGET_COLUMNS.forEach(targetCol => { |
|
|
const index = actualHeaders.indexOf(targetCol); |
|
|
colIndices[targetCol] = index; |
|
|
}); |
|
|
|
|
|
|
|
|
const rows = [...table.querySelectorAll('tbody tr')]; |
|
|
const parsedData = []; |
|
|
|
|
|
rows.forEach(row => { |
|
|
const cells = [...row.querySelectorAll('td')]; |
|
|
const rowData = {}; |
|
|
let hasData = false; |
|
|
|
|
|
TARGET_COLUMNS.forEach(targetCol => { |
|
|
const index = colIndices[targetCol]; |
|
|
if (index !== -1 && cells[index]) { |
|
|
rowData[targetCol] = cells[index].textContent.trim(); |
|
|
hasData = true; |
|
|
} else { |
|
|
rowData[targetCol] = ''; |
|
|
} |
|
|
}); |
|
|
|
|
|
if (hasData) { |
|
|
parsedData.push(rowData); |
|
|
} |
|
|
}); |
|
|
|
|
|
return parsedData; |
|
|
} catch (error) { |
|
|
console.error('解析 HTML 表格失敗:', error); |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderTable(data) { |
|
|
tableBody.innerHTML = ''; |
|
|
|
|
|
if (data.length === 0) { |
|
|
tableBody.innerHTML = `<tr><td colspan="${TARGET_COLUMNS.length}" class="px-4 py-6 text-center text-gray-500">查無資料</td></tr>`; |
|
|
return; |
|
|
} |
|
|
|
|
|
const fragment = document.createDocumentFragment(); |
|
|
data.forEach(stock => { |
|
|
const tr = document.createElement('tr'); |
|
|
tr.className = 'hover:bg-gray-50'; |
|
|
|
|
|
let cellsHTML = ''; |
|
|
TARGET_COLUMNS.forEach(header => { |
|
|
cellsHTML += `<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-700">${stock[header] || ''}</td>`; |
|
|
}); |
|
|
tr.innerHTML = cellsHTML; |
|
|
fragment.appendChild(tr); |
|
|
}); |
|
|
|
|
|
tableBody.appendChild(fragment); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function performSearch() { |
|
|
const keyword = searchInput.value.trim(); |
|
|
|
|
|
if (!keyword) { |
|
|
|
|
|
renderTable(allStockData); |
|
|
statusMessage.textContent = `顯示所有 ${allStockData.length} 筆資料。`; |
|
|
statusMessage.className = 'my-4 p-3 bg-green-100 text-green-800 rounded-lg text-center font-medium'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
addHistory(keyword); |
|
|
|
|
|
|
|
|
const lowerKeyword = keyword.toLowerCase(); |
|
|
const filteredData = allStockData.filter(stock => { |
|
|
const nameMatch = stock['公司'] && stock['公司'].toLowerCase().includes(lowerKeyword); |
|
|
const codeMatch = stock['代號'] && stock['代號'].includes(keyword); |
|
|
return nameMatch || codeMatch; |
|
|
}); |
|
|
|
|
|
|
|
|
renderTable(filteredData); |
|
|
|
|
|
|
|
|
if (filteredData.length === 0) { |
|
|
statusMessage.textContent = `找不到關於 "${keyword}" 的資料。`; |
|
|
statusMessage.className = 'my-4 p-3 bg-yellow-100 text-yellow-800 rounded-lg text-center font-medium'; |
|
|
} else { |
|
|
statusMessage.textContent = `找到 ${filteredData.length} 筆關於 "${keyword}" 的資料。`; |
|
|
statusMessage.className = 'my-4 p-3 bg-green-100 text-green-800 rounded-lg text-center font-medium'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function loadHistory() { |
|
|
const stored = localStorage.getItem(HISTORY_KEY); |
|
|
if (stored) { |
|
|
searchHistory = JSON.parse(stored); |
|
|
renderHistory(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addHistory(keyword) { |
|
|
|
|
|
const existingIndex = searchHistory.indexOf(keyword); |
|
|
if (existingIndex > -1) { |
|
|
searchHistory.splice(existingIndex, 1); |
|
|
} |
|
|
|
|
|
|
|
|
searchHistory.unshift(keyword); |
|
|
|
|
|
|
|
|
if (searchHistory.length > MAX_HISTORY) { |
|
|
searchHistory = searchHistory.slice(0, MAX_HISTORY); |
|
|
} |
|
|
|
|
|
|
|
|
localStorage.setItem(HISTORY_KEY, JSON.stringify(searchHistory)); |
|
|
|
|
|
|
|
|
renderHistory(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderHistory() { |
|
|
|
|
|
historyContainer.innerHTML = '<strong class="mr-2">搜尋記錄:</strong>'; |
|
|
|
|
|
searchHistory.forEach(term => { |
|
|
const tag = document.createElement('span'); |
|
|
tag.textContent = term; |
|
|
tag.className = 'clickable-tag bg-gray-200 text-gray-700 px-3 py-1 rounded-full text-xs font-medium shadow-sm'; |
|
|
|
|
|
|
|
|
tag.onclick = () => { |
|
|
searchInput.value = term; |
|
|
performSearch(); |
|
|
}; |
|
|
historyContainer.appendChild(tag); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
searchButton.addEventListener('click', performSearch); |
|
|
searchInput.addEventListener('keyup', (e) => { |
|
|
if (e.key === 'Enter') { |
|
|
performSearch(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
loadHistory(); |
|
|
fetchData(); |
|
|
}); |
|
|
|
|
|
</script> |
|
|
</body> |
|
|
</html> |