NFCcreate-web/templates/manual_test.html
2025-09-25 19:04:00 +08:00

1157 lines
44 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NT4H 手動測試工具</title>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
min-height: 100vh;
padding: 20px;
color: #f1f5f9;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: rgba(15, 23, 42, 0.95);
border-radius: 24px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4);
overflow: hidden;
border: 1px solid rgba(148, 163, 184, 0.1);
backdrop-filter: blur(20px);
}
.header {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 50%, #1e40af 100%);
color: white;
padding: 40px;
text-align: center;
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
animation: shimmer 3s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.header h1 {
font-size: 2.8em;
margin-bottom: 12px;
font-weight: 600;
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
letter-spacing: -0.02em;
}
.header p {
font-size: 1.1em;
opacity: 0.9;
}
.nav-bar {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
padding: 20px 40px;
border-bottom: 1px solid rgba(148, 163, 184, 0.2);
}
.nav-bar a {
color: #60a5fa;
text-decoration: none;
margin-right: 30px;
padding: 12px 20px;
border-radius: 12px;
transition: all 0.3s ease;
font-weight: 500;
font-size: 15px;
}
.nav-bar a:hover {
background: rgba(96, 165, 250, 0.15);
color: #93c5fd;
transform: translateY(-1px);
}
.nav-bar a.active {
background: rgba(96, 165, 250, 0.25);
color: #dbeafe;
box-shadow: 0 4px 12px rgba(96, 165, 250, 0.2);
}
.content {
padding: 40px;
}
.main-layout {
display: grid;
grid-template-columns: 1fr 400px;
gap: 30px;
margin-bottom: 30px;
}
.test-sections {
display: flex;
flex-direction: column;
gap: 30px;
height: 600px;
overflow-y: auto;
}
.test-section {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
padding: 30px;
border-radius: 20px;
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.test-section h3 {
color: #60a5fa;
margin-bottom: 25px;
font-size: 1.3em;
font-weight: 600;
text-shadow: 0 0 15px rgba(96, 165, 250, 0.3);
display: flex;
align-items: center;
gap: 12px;
}
.test-section h3::before {
content: '🔧';
font-size: 1.2em;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #d1d5db;
font-size: 14px;
}
.form-group input, .form-group select, .form-group textarea {
width: 100%;
padding: 14px 18px;
border: 2px solid rgba(148, 163, 184, 0.3);
border-radius: 12px;
font-size: 14px;
transition: all 0.3s ease;
background: rgba(15, 23, 42, 0.5);
color: #f1f5f9;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
margin-top: 15px;
}
.checkbox-group input[type="checkbox"] {
width: auto;
margin: 0;
}
.btn {
padding: 12px 25px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
position: relative;
overflow: hidden;
min-width: 120px;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.btn:hover::before {
left: 100%;
}
.btn-primary {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: white;
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.4);
}
.btn-secondary {
background: linear-gradient(135deg, #64748b 0%, #475569 100%);
color: white;
}
.btn-secondary:hover {
background: linear-gradient(135deg, #475569 0%, #334155 100%);
transform: translateY(-1px);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.output-section {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
border-radius: 20px;
padding: 25px;
height: 600px;
position: relative;
border: 1px solid rgba(34, 197, 94, 0.3);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
}
.output-section h3 {
color: #22c55e;
margin-bottom: 15px;
font-size: 1.1em;
font-weight: 600;
text-shadow: 0 0 10px rgba(34, 197, 94, 0.3);
}
.output-content {
background: #000;
border-radius: 10px;
padding: 15px;
height: calc(100% - 120px);
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
color: #22c55e;
border: 1px solid rgba(34, 197, 94, 0.2);
}
.output-input-section {
margin-top: 10px;
}
.input-group {
display: flex;
gap: 10px;
align-items: center;
}
.input-group input {
flex: 1;
padding: 10px 15px;
border: 2px solid rgba(251, 191, 36, 0.4);
border-radius: 8px;
font-size: 14px;
background: rgba(0, 0, 0, 0.3);
color: #e0e0e0;
transition: all 0.3s ease;
}
.input-group input:focus {
outline: none;
border-color: #f59e0b;
box-shadow: 0 0 15px rgba(245, 158, 11, 0.3);
}
.input-group .btn {
padding: 10px 20px;
font-size: 12px;
min-width: 100px;
}
.output-line {
margin-bottom: 2px;
word-wrap: break-word;
}
.command-line {
color: #fbbf24;
font-weight: bold;
}
.success-line {
color: #22c55e;
}
.error-line {
color: #ef4444;
}
.info-line {
color: #3b82f6;
}
.help-section {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
padding: 30px;
border-radius: 20px;
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
margin-top: 30px;
}
.help-section h3 {
color: #60a5fa;
margin-bottom: 20px;
font-size: 1.3em;
font-weight: 600;
text-shadow: 0 0 15px rgba(96, 165, 250, 0.3);
}
.help-content {
color: #d1d5db;
line-height: 1.6;
}
.help-content h4 {
color: #60a5fa;
margin: 15px 0 8px 0;
}
.help-content code {
background: rgba(15, 23, 42, 0.5);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
color: #60a5fa;
}
.help-content ul {
margin-left: 20px;
margin-bottom: 15px;
}
.help-content li {
margin-bottom: 5px;
}
@media (max-width: 768px) {
.main-layout {
grid-template-columns: 1fr;
}
.form-row {
grid-template-columns: 1fr;
}
}
/* 浮動視窗樣式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: rgba(15, 23, 42, 0.95);
border: 1px solid rgba(148, 163, 184, 0.2);
border-radius: 20px;
padding: 35px;
min-width: 400px;
max-width: 500px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.6);
backdrop-filter: blur(20px);
}
.modal h3 {
color: #60a5fa;
margin-bottom: 25px;
text-align: center;
font-weight: 600;
font-size: 1.4em;
}
.modal-input-group {
margin-bottom: 20px;
}
.modal-input-group label {
display: block;
margin-bottom: 8px;
color: #e0e0e0;
font-weight: 500;
}
.modal-input-group input {
width: 100%;
padding: 14px 18px;
border: 2px solid rgba(148, 163, 184, 0.3);
border-radius: 12px;
font-size: 14px;
background: rgba(15, 23, 42, 0.5);
color: #f1f5f9;
box-sizing: border-box;
}
.modal-input-group input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.modal-buttons {
display: flex;
gap: 15px;
justify-content: flex-end;
}
.modal-buttons .btn {
min-width: 80px;
}
.btn-secondary {
background: linear-gradient(135deg, #64748b 0%, #475569 100%);
color: white;
}
.btn-secondary:hover {
background: linear-gradient(135deg, #475569 0%, #334155 100%);
transform: translateY(-1px);
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧪 NFC 手動測試工具</h1>
<p>專業的 NFC 卡片手動測試與診斷系統</p>
</div>
<div class="nav-bar">
<a href="/" class="nav-link">主頁面</a>
<a href="/manual_test" class="nav-link active">手動測試</a>
</div>
<div class="content">
<div class="main-layout">
<div class="output-section">
<h3>📊 執行輸出</h3>
<div class="output-content" id="outputContent">
<div class="output-line">等待執行命令...</div>
</div>
<div class="output-input-section">
<div class="input-group">
<input type="text" id="quickCommandInput"
placeholder="快速輸入指令,例如: ./nt4h_c_example verify --key 1"
style="font-family: 'Courier New', monospace; margin-top: 10px;">
<button class="btn btn-primary" id="quickExecuteBtn" style="margin-top: 10px;">快速執行</button>
</div>
</div>
</div>
<div class="test-sections">
<!-- 快速 SDM 設定 -->
<div class="test-section">
<h3>🔐 快速 SDM 設定</h3>
<form id="setsdmForm">
<div class="form-row">
<div class="form-group">
<label for="setsdmUrlIndex">URL 索引</label>
<select id="setsdmUrlIndex" name="urlIndex" required>
<option value="">選擇 URL</option>
</select>
</div>
<div class="form-group">
<label for="setsdmKeyIndex">金鑰索引</label>
<select id="setsdmKeyIndex" name="keyIndex">
<option value="">選擇金鑰</option>
</select>
</div>
</div>
<div class="checkbox-group">
<input type="checkbox" id="setsdmQuiet" name="quietMode">
<label for="setsdmQuiet">安靜模式</label>
</div>
<button type="submit" class="btn btn-primary">設定 SDM</button>
</form>
</div>
<!-- NDEF 操作 -->
<div class="test-section">
<h3>📄 NDEF 操作 (無 SDM)</h3>
<form id="ndefForm">
<div class="form-group">
<label for="ndefUrlIndex">URL 索引</label>
<div style="display: flex; flex-direction: row; gap: 12px; align-items: center;">
<select id="ndefUrlIndex" name="urlIndex" required>
<option value="">選擇 URL</option>
</select>
<button class="btn btn-secondary" id="edit-url-btn" style="padding: 10px 15px; font-size: 12px;">✏️ 編輯</button>
</div>
</div>
<div class="form-row">
<button type="button" class="btn btn-primary" id="writendefBtn">寫入 NDEF</button>
<button type="button" class="btn btn-primary" id="readndefBtn">讀取 NDEF</button>
</div>
</form>
</div>
<!-- 變更 AES 金鑰 -->
<div class="test-section">
<h3>🔑 變更 AES 金鑰</h3>
<form id="changekeyForm">
<div class="form-row">
<div class="form-group">
<label for="authKey">認證金鑰索引</label>
<select id="authKey" name="authKey" required>
<option value="">選擇認證金鑰</option>
</select>
</div>
<div class="form-group">
<label for="newKey">新金鑰索引</label>
<select id="newKey" name="newKey" required>
<option value="">選擇新金鑰</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="oldKey">舊金鑰索引</label>
<select id="oldKey" name="oldKey" required>
<option value="">選擇舊金鑰</option>
</select>
</div>
<div class="form-group">
<label for="keyNo">金鑰編號 (0-4)</label>
<select id="keyNo" name="keyNo">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</div>
</div>
<div class="checkbox-group">
<input type="checkbox" id="changekeyQuiet" name="quietMode">
<label for="changekeyQuiet">安靜模式</label>
</div>
<button type="submit" class="btn btn-primary">變更金鑰</button>
</form>
</div>
<!-- 讀取 UID -->
<div class="test-section">
<h3>📖 讀取 UID</h3>
<form id="getuidForm">
<div class="form-group">
<label for="getuidKeyIndex">金鑰索引</label>
<select id="getuidKeyIndex" name="keyIndex">
<option value="">選擇金鑰</option>
</select>
</div>
<div class="checkbox-group">
<input type="checkbox" id="getuidQuiet" name="quietMode">
<label for="getuidQuiet">安靜模式</label>
</div>
<button type="submit" class="btn btn-primary">讀取 UID</button>
</form>
</div>
</div>
</div>
<!-- 幫助說明 -->
<div class="help-section">
<h3>📚 NT4H CLI 工具說明</h3>
<div class="help-content">
<h4>用法:</h4>
<code>./nt4h_c_example &lt;命令&gt; [選項]</code>
<h4>可用命令:</h4>
<ul>
<li><code>verify [--key &lt;索引&gt;]</code> - CMAC 驗證現有標籤</li>
<li><code>setsdm [--url &lt;索引&gt;] [--key &lt;索引&gt;]</code> - 快速 SDM 設定</li>
<li><code>writendef --url &lt;URL&gt; 或 --url-index &lt;索引&gt;</code> - 寫入單純 NDEF (無 SDM)</li>
<li><code>changekey [--auth-key &lt;索引&gt;] [--new-key &lt;索引&gt;] [--old-key &lt;索引&gt;] [--key-no &lt;編號&gt;]</code> - 變更 AES 金鑰</li>
<li><code>getuid [--key &lt;索引&gt;]</code> - 讀取 UID</li>
<li><code>readndef</code> - 讀取單純 NDEF (無 SDM)</li>
<li><code>help</code> - 顯示完整說明</li>
</ul>
<h4>常用選項:</h4>
<ul>
<li><code>--key &lt;索引&gt;</code> - 使用 keys.txt 中第 &lt;索引&gt; 個金鑰 (1-based)</li>
<li><code>--url &lt;索引&gt;</code> - 使用 urls.txt 中第 &lt;索引&gt; 個 URL (1-based)</li>
<li><code>--url-index &lt;索引&gt;</code> - 手動驗證模式中使用的 URL 索引 (1-based)</li>
<li><code>--auth-key &lt;索引&gt;</code> - 使用 keys.txt 中第 &lt;索引&gt; 個金鑰作為認證金鑰</li>
<li><code>--new-key &lt;索引&gt;</code> - 使用 keys.txt 中第 &lt;索引&gt; 個金鑰作為新金鑰</li>
<li><code>--old-key &lt;索引&gt;</code> - 使用 keys.txt 中第 &lt;索引&gt; 個金鑰作為舊金鑰</li>
<li><code>--key-no &lt;編號&gt;</code> - 指定要變更的金鑰編號 (0-4)</li>
<li><code>--manual</code> - 手動驗證模式</li>
<li><code>--uid &lt;UID&gt;</code> - 指定 UID (14位十六進位)</li>
<li><code>--ctr &lt;計數器&gt;</code> - 指定計數器 (6位十六進位)</li>
<li><code>--cmac &lt;CMAC&gt;</code> - 指定 CMAC (16位十六進位)</li>
<li><code>--quiet, -q</code> - 安靜模式,只輸出 SUCCEED 或 FAILED</li>
</ul>
<h4>常用範例:</h4>
<ul>
<li><code>./nt4h_c_example verify --key 1</code> - 使用金鑰1進行CMAC驗證</li>
<li><code>./nt4h_c_example verify --manual --uid 0456735AD51F90 --ctr 0000B1 --cmac C2DEEE0FF07E7EC4</code> - 手動驗證模式</li>
<li><code>./nt4h_c_example verify --manual --url "https://nodered.contree.app/nfc?uid=0456735AD51F90&ctr=0000B1&cmac=C2DEEE0FF07E7EC4"</code> - 使用完整URL驗證</li>
<li><code>./nt4h_c_example setsdm --quiet --url 1 --key 2</code> - 安靜模式設定SDM</li>
<li><code>./nt4h_c_example getuid --quiet --key 3</code> - 安靜模式讀取UID</li>
<li><code>./nt4h_c_example changekey --auth-key 1 --new-key 2 --old-key 1 --key-no 1</code> - 變更金鑰</li>
<li><code>./nt4h_c_example writendef --url "https://example.com/my-url"</code> - 寫入自定義URL</li>
<li><code>./nt4h_c_example help</code> - 顯示完整說明</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 文字編輯URL浮動視窗 -->
<div class="modal-overlay" id="textEditUrlModal">
<div class="modal" style="min-width: 600px; max-width: 800px; max-height: 80vh;">
<h3>📝 編輯 URLs.txt</h3>
<div class="modal-input-group">
<label for="urls-textarea">URLs 內容每行一個URL</label>
<p style="color: #94a3b8; font-size: 12px; margin: 5px 0;">注意前三行為系統保留此處顯示從第四行開始的URL</p>
<textarea id="urls-textarea" style="width: 100%; height: 400px; padding: 15px; border: 2px solid rgba(148, 163, 184, 0.3); border-radius: 12px; font-size: 14px; background: rgba(15, 23, 42, 0.5); color: #f1f5f9; box-sizing: border-box; font-family: 'Courier New', monospace; resize: vertical; line-height: 1.4;" placeholder="請輸入URLs每行一個URL"></textarea>
</div>
<div class="modal-buttons">
<button class="btn btn-secondary" id="cancel-text-edit-btn">取消</button>
<button class="btn btn-primary" id="save-text-edit-btn">儲存</button>
</div>
</div>
</div>
<script>
// Socket.IO 連接
const socket = io();
// 全域變數
let keys = [];
let urls = [];
let isConnected = false;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
// 初始化
document.addEventListener('DOMContentLoaded', function() {
loadKeys();
loadUrls();
setupSocketIO();
setupForms();
});
// 載入金鑰列表
async function loadKeys() {
try {
const response = await fetch('/api/keys');
const data = await response.json();
if (data.success) {
keys = data.keys;
populateKeySelects();
}
} catch (error) {
console.error('載入金鑰失敗:', error);
}
}
// 載入 URL 列表
async function loadUrls() {
try {
const response = await fetch('/api/urls');
const data = await response.json();
if (data.success) {
urls = data.urls;
populateUrlSelects();
}
} catch (error) {
console.error('載入 URLs 失敗:', error);
}
}
// 填充金鑰選擇器
function populateKeySelects() {
const keySelects = document.querySelectorAll('select[id*="Key"]');
keySelects.forEach(select => {
select.innerHTML = '<option value="">選擇金鑰</option>';
keys.forEach((key, index) => {
const option = document.createElement('option');
option.value = index + 1;
option.textContent = `金鑰 ${index + 1}: ${key.substring(0, 8)}...`;
select.appendChild(option);
});
});
}
// 填充 URL 選擇器
function populateUrlSelects() {
const urlSelects = document.querySelectorAll('select[id*="Url"]');
urlSelects.forEach(select => {
select.innerHTML = '<option value="">選擇 URL</option>';
urls.forEach((url, index) => {
const option = document.createElement('option');
option.value = index + 1;
option.textContent = `URL ${index + 1}: ${url.substring(0, 30)}...`;
select.appendChild(option);
});
});
}
// 設定 Socket.IO
function setupSocketIO() {
socket.on('connect', function() {
console.log('WebSocket 已連接');
isConnected = true;
reconnectAttempts = 0;
addOutputLine('🔗 WebSocket 已連接', 'info');
});
socket.on('disconnect', function() {
console.log('WebSocket 已斷開');
isConnected = false;
addOutputLine('❌ WebSocket 已斷開', 'error');
});
socket.on('connect_error', function(error) {
console.error('WebSocket 連接錯誤:', error);
addOutputLine(`❌ WebSocket 連接錯誤: ${error.message}`, 'error');
});
socket.on('script_output', function(data) {
console.log('收到 script_output:', data);
addColoredOutputLine(data.line);
});
socket.on('script_finished', function(data) {
console.log('收到 script_finished:', data);
addOutputLine(`=== ${data.message} ===`, 'success');
});
// 定期檢查連接狀態
setInterval(() => {
if (!isConnected && reconnectAttempts < maxReconnectAttempts) {
console.log('嘗試重新連接 WebSocket...');
reconnectAttempts++;
socket.connect();
}
}, 5000);
}
// 設定表單事件
function setupForms() {
// SDM 設定表單
document.getElementById('setsdmForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = {
url_index: formData.get('urlIndex'),
key_index: formData.get('keyIndex'),
quiet_mode: formData.get('quietMode') === 'on'
};
executeManualSetsdm(data);
});
// 變更金鑰表單
document.getElementById('changekeyForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = {
auth_key: formData.get('authKey'),
new_key: formData.get('newKey'),
old_key: formData.get('oldKey'),
key_no: formData.get('keyNo'),
quiet_mode: formData.get('quietMode') === 'on'
};
executeManualChangekey(data);
});
// 讀取 UID 表單
document.getElementById('getuidForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = {
key_index: formData.get('keyIndex'),
quiet_mode: formData.get('quietMode') === 'on'
};
executeManualGetuid(data);
});
}
// 執行 SDM 設定
async function executeManualSetsdm(data) {
try {
const response = await fetch('/api/manual_setsdm', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
addOutputLine(`開始執行: ${result.message}`, 'command');
} else {
addOutputLine(`錯誤: ${result.message}`, 'error');
}
} catch (error) {
addOutputLine(`執行失敗: ${error.message}`, 'error');
}
}
// 執行寫入 NDEF
async function executeManualWritendef(data) {
try {
const response = await fetch('/api/manual_writendef', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
addOutputLine(`開始執行: ${result.message}`, 'command');
} else {
addOutputLine(`錯誤: ${result.message}`, 'error');
}
} catch (error) {
addOutputLine(`執行失敗: ${error.message}`, 'error');
}
}
// 執行變更金鑰
async function executeManualChangekey(data) {
try {
const response = await fetch('/api/manual_changekey', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
addOutputLine(`開始執行: ${result.message}`, 'command');
} else {
addOutputLine(`錯誤: ${result.message}`, 'error');
}
} catch (error) {
addOutputLine(`執行失敗: ${error.message}`, 'error');
}
}
// 執行讀取 UID
async function executeManualGetuid(data) {
try {
const response = await fetch('/api/manual_getuid', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
addOutputLine(`開始執行: ${result.message}`, 'command');
} else {
addOutputLine(`錯誤: ${result.message}`, 'error');
}
} catch (error) {
addOutputLine(`執行失敗: ${error.message}`, 'error');
}
}
// 執行讀取 NDEF
async function executeManualReadndef() {
try {
const response = await fetch('/api/manual_readndef', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
addOutputLine(`開始執行: ${result.message}`, 'command');
} else {
addOutputLine(`錯誤: ${result.message}`, 'error');
}
} catch (error) {
addOutputLine(`執行失敗: ${error.message}`, 'error');
}
}
// 執行自定義指令
async function executeCustomCommand(data) {
try {
const response = await fetch('/api/manual_custom_command', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
addOutputLine(`開始執行自定義指令: ${result.message}`, 'command');
if (data.description) {
addOutputLine(`指令說明: ${data.description}`, 'info');
}
addOutputLine(`完整指令: ${data.command}`, 'command');
} else {
addOutputLine(`錯誤: ${result.message}`, 'error');
}
} catch (error) {
addOutputLine(`執行失敗: ${error.message}`, 'error');
}
}
// 添加輸出行
function addOutputLine(text, type = 'normal') {
const outputContent = document.getElementById('outputContent');
const line = document.createElement('div');
line.className = 'output-line';
if (type === 'command') {
line.classList.add('command-line');
} else if (type === 'success') {
line.classList.add('success-line');
} else if (type === 'error') {
line.classList.add('error-line');
} else if (type === 'info') {
line.classList.add('info-line');
}
line.textContent = text;
outputContent.appendChild(line);
outputContent.scrollTop = outputContent.scrollHeight;
}
// 添加帶顏色的輸出行
function addColoredOutputLine(text) {
const outputContent = document.getElementById('outputContent');
const line = document.createElement('div');
line.className = 'output-line';
line.innerHTML = parseAnsiColors(text);
outputContent.appendChild(line);
outputContent.scrollTop = outputContent.scrollHeight;
}
// 解析 ANSI 顏色代碼
function parseAnsiColors(text) {
// 使用更簡單的方法處理 ANSI 顏色代碼
let result = text;
// 處理常見的 ANSI 顏色代碼
result = result.replace(/\x1b\[0;31m/g, '<span style="color: #ff6b6b;">'); // 紅色
result = result.replace(/\x1b\[0;32m/g, '<span style="color: #51cf66;">'); // 綠色
result = result.replace(/\x1b\[0;33m/g, '<span style="color: #ffd43b;">'); // 黃色
result = result.replace(/\x1b\[0;34m/g, '<span style="color: #74c0fc;">'); // 藍色
result = result.replace(/\x1b\[1;33m/g, '<span style="color: #ffd43b; font-weight: bold;">'); // 粗體黃色
result = result.replace(/\x1b\[1;31m/g, '<span style="color: #ff6b6b; font-weight: bold;">'); // 粗體紅色
result = result.replace(/\x1b\[1;32m/g, '<span style="color: #51cf66; font-weight: bold;">'); // 粗體綠色
result = result.replace(/\x1b\[1;34m/g, '<span style="color: #74c0fc; font-weight: bold;">'); // 粗體藍色
result = result.replace(/\x1b\[0m/g, '</span>'); // 重置顏色
// 處理未閉合的 span 標籤
const openSpans = (result.match(/<span/g) || []).length;
const closeSpans = (result.match(/<\/span>/g) || []).length;
for (let i = 0; i < openSpans - closeSpans; i++) {
result += '</span>';
}
return result;
}
// 清空輸出
function clearOutput() {
document.getElementById('outputContent').innerHTML = '<div class="output-line">等待執行命令...</div>';
}
// 文字編輯URL相關元素
const textEditUrlModal = document.getElementById('textEditUrlModal');
const urlsTextarea = document.getElementById('urls-textarea');
const cancelTextEditBtn = document.getElementById('cancel-text-edit-btn');
const saveTextEditBtn = document.getElementById('save-text-edit-btn');
// 顯示文字編輯URL浮動視窗
function showTextEditUrlModal() {
textEditUrlModal.style.display = 'flex';
loadUrlsToTextarea();
}
// 隱藏文字編輯URL浮動視窗
function hideTextEditUrlModal() {
textEditUrlModal.style.display = 'none';
urlsTextarea.value = '';
}
// 載入URLs到文字編輯區域從第四行開始
function loadUrlsToTextarea() {
fetch('/api/urls').then(r=>r.json()).then(data=>{
if(data.success && data.urls.length > 0){
// 只顯示從第四行開始的URL索引3開始
const editableUrls = data.urls.slice(3);
urlsTextarea.value = editableUrls.join('\n');
} else {
urlsTextarea.value = '';
}
});
}
// 設定編輯按鈕事件
document.addEventListener('DOMContentLoaded', function() {
// NDEF 操作按鈕事件
document.getElementById('writendefBtn').addEventListener('click', function() {
const urlIndex = document.getElementById('ndefUrlIndex').value;
if (urlIndex) {
executeManualWritendef({ url_index: urlIndex });
} else {
addOutputLine('請選擇 URL', 'error');
}
});
document.getElementById('readndefBtn').addEventListener('click', function() {
executeManualReadndef();
});
// 快速執行按鈕事件
document.getElementById('quickExecuteBtn').addEventListener('click', function() {
const command = document.getElementById('quickCommandInput').value.trim();
if (command) {
executeCustomCommand({
command: command,
description: '快速執行指令'
});
document.getElementById('quickCommandInput').value = '';
} else {
addOutputLine('請輸入指令', 'error');
}
});
// 快速執行輸入框 Enter 鍵事件
document.getElementById('quickCommandInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
document.getElementById('quickExecuteBtn').click();
}
});
document.getElementById('edit-url-btn').addEventListener('click', function(e) {
e.preventDefault();
showTextEditUrlModal();
});
// 取消文字編輯按鈕事件
cancelTextEditBtn.addEventListener('click', function() {
hideTextEditUrlModal();
});
// 儲存文字編輯按鈕事件
saveTextEditBtn.addEventListener('click', function() {
const urlsContent = urlsTextarea.value.trim();
if (!urlsContent) {
alert('URLs內容不得為空');
return;
}
// 先獲取前三行,然後合併編輯的內容
fetch('/api/urls').then(r=>r.json()).then(data=>{
if(data.success && data.urls.length >= 3){
// 保留前三行
const firstThreeUrls = data.urls.slice(0, 3);
// 編輯的內容(從第四行開始)
const editedUrls = urlsContent.split('\n').filter(line => line.trim());
// 合併所有URL
const allUrls = [...firstThreeUrls, ...editedUrls];
fetch('/api/save_urls_file', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({urls_content: allUrls.join('\n')})
}).then(r=>r.json()).then(data=>{
if(data.success) {
addOutputLine(`URLs文件保存成功共保存了 ${data.urls_count} 個URL`, 'success');
hideTextEditUrlModal();
loadUrls(); // 重新載入URL選項
} else {
addOutputLine(`保存失敗: ${data.message}`, 'error');
}
});
} else {
addOutputLine('無法讀取現有URLs請重試', 'error');
}
});
});
// 按ESC鍵關閉浮動視窗
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && textEditUrlModal.style.display === 'flex') {
hideTextEditUrlModal();
}
});
// 點擊浮動視窗背景關閉浮動視窗
textEditUrlModal.addEventListener('click', function(e) {
if (e.target === textEditUrlModal) {
hideTextEditUrlModal();
}
});
});
</script>
</body>
</html>