Initial commit: caddy static file server workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
201
caddy/www/html5_qrcode_invoice_parser.html
Normal file
201
caddy/www/html5_qrcode_invoice_parser.html
Normal file
@@ -0,0 +1,201 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>QR Code Scanner (固定320x240)</title>
|
||||
|
||||
<!-- 載入 HTML5Qrcode 函式庫 -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5-qrcode/2.3.8/html5-qrcode.min.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#cameraSelector {
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* 關鍵:固定掃描容器大小,並使用 overflow:hidden; */
|
||||
#reader-container {
|
||||
position: relative; /* 讓內部絕對定位有效 */
|
||||
width: 320px; /* 固定寬度 */
|
||||
height: 240px; /* 固定高度 */
|
||||
border: 2px solid #000;
|
||||
background-color: #ccc; /* 相機讀取前的底色 */
|
||||
overflow: hidden; /* 超出的部分被裁切 */
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* #reader 直接充滿容器,不再撐大容器 */
|
||||
#reader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 讓攝影機畫面被裁切到 320x240 之中 */
|
||||
video {
|
||||
object-fit: cover; /* cover: 視訊填滿容器,超出部分裁切 */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 顯示掃描 or 解析結果 */
|
||||
#result {
|
||||
width: 320px;
|
||||
max-height: 150px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #fff;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#copyResult {
|
||||
font-size: 16px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>QR Code Scanner</h1>
|
||||
|
||||
<!-- 下拉式選擇攝影機 -->
|
||||
<select id="cameraSelector"></select>
|
||||
|
||||
<!-- 掃描區(固定 320x240,不因手機長比例而變形) -->
|
||||
<div id="reader-container">
|
||||
<div id="reader"></div>
|
||||
</div>
|
||||
|
||||
<!-- 顯示掃描或解析結果 -->
|
||||
<div id="result">請將 QR Code 對準掃描框。</div>
|
||||
|
||||
<!-- 複製結果按鈕 -->
|
||||
<button id="copyResult">複製結果</button>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", async function () {
|
||||
const resultDiv = document.getElementById("result");
|
||||
const cameraSelector = document.getElementById("cameraSelector");
|
||||
const copyButton = document.getElementById("copyResult");
|
||||
const html5QrCode = new Html5Qrcode("reader");
|
||||
let currentCameraId = null;
|
||||
|
||||
// 取得可用攝影機清單
|
||||
try {
|
||||
const cameras = await Html5Qrcode.getCameras();
|
||||
if (cameras.length === 0) {
|
||||
resultDiv.textContent = "未找到攝影機。";
|
||||
return;
|
||||
}
|
||||
cameraSelector.innerHTML = cameras
|
||||
.map((cam, idx) => `<option value="${cam.id}">${cam.label || "相機 " + (idx + 1)}</option>`)
|
||||
.join("");
|
||||
|
||||
// 預設用第一個攝影機
|
||||
currentCameraId = cameras[0].id;
|
||||
await startScanner(currentCameraId);
|
||||
} catch (err) {
|
||||
resultDiv.textContent = `無法取得攝影機:${err.message}`;
|
||||
}
|
||||
|
||||
// 切換攝影機
|
||||
cameraSelector.addEventListener("change", async () => {
|
||||
const selected = cameraSelector.value;
|
||||
if (selected !== currentCameraId) {
|
||||
await html5QrCode.stop();
|
||||
currentCameraId = selected;
|
||||
startScanner(currentCameraId);
|
||||
}
|
||||
});
|
||||
|
||||
// 開始掃描
|
||||
async function startScanner(cameraId) {
|
||||
try {
|
||||
await html5QrCode.start(
|
||||
{ deviceId: cameraId },
|
||||
{
|
||||
fps: 20,
|
||||
qrbox: { width: 200, height: 200 }
|
||||
},
|
||||
(decodedText) => {
|
||||
displayRawResult(decodedText);
|
||||
parseQRCode(decodedText);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
resultDiv.textContent = `無法啟動掃描器:${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 顯示「原始掃描」內容
|
||||
function displayRawResult(decodedText) {
|
||||
resultDiv.innerHTML = `<strong>識別到的 QR Code:</strong><br>${decodedText}`;
|
||||
resultDiv.scrollTop = 0;
|
||||
}
|
||||
|
||||
// 嘗試解析台灣電子發票
|
||||
function parseQRCode(data) {
|
||||
try {
|
||||
const raw = data.trim();
|
||||
const invoiceNumber = raw.slice(0, 10);
|
||||
const issueDate = raw.slice(10, 17);
|
||||
const year = parseInt(issueDate.slice(0, 3)) + 1911;
|
||||
const month = issueDate.slice(3, 5);
|
||||
const day = issueDate.slice(5, 7);
|
||||
const formattedDate = `${year}-${month}-${day}`;
|
||||
const randomCode = raw.slice(17, 21);
|
||||
const untaxedAmount = parseInt(raw.slice(21, 29), 16);
|
||||
const totalAmount = parseInt(raw.slice(29, 37), 16);
|
||||
const buyerTaxId = raw.slice(37, 45);
|
||||
const sellerTaxId = raw.slice(45, 53);
|
||||
const encryptedCode = raw.slice(53, 77);
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<strong>發票號碼:</strong> ${invoiceNumber}<br>
|
||||
<strong>開立日期:</strong> ${formattedDate}<br>
|
||||
<strong>隨機碼:</strong> ${randomCode}<br>
|
||||
<strong>未稅金額:</strong> ${untaxedAmount}<br>
|
||||
<strong>總計金額:</strong> ${totalAmount}<br>
|
||||
<strong>買方統一編號:</strong> ${buyerTaxId}<br>
|
||||
<strong>賣方統一編號:</strong> ${sellerTaxId}<br>
|
||||
<strong>加密驗證資訊:</strong> ${encryptedCode}<br>
|
||||
`;
|
||||
} catch (e) {
|
||||
// 如果不是符合格式,就保持原始掃描訊息
|
||||
}
|
||||
}
|
||||
|
||||
// 複製結果
|
||||
copyButton.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(resultDiv.innerText).then(
|
||||
() => alert("結果已複製到剪貼簿!"),
|
||||
(err) => alert("複製失敗:" + err)
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user