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

1778 lines
63 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>NFC 生產工具</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;
}
.top-section {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
padding: 30px;
border-radius: 20px;
margin-bottom: 30px;
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.top-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);
}
.main-layout {
display: grid;
grid-template-columns: 1fr 400px;
gap: 30px;
margin-bottom: 30px;
}
.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);
}
.control-panel {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
border-radius: 20px;
padding: 30px;
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
height: 600px;
overflow-y: auto;
}
.control-panel h3 {
color: #60a5fa;
margin-bottom: 25px;
font-size: 1.3em;
font-weight: 600;
text-shadow: 0 0 15px rgba(96, 165, 250, 0.3);
}
.api-section {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
padding: 25px;
border-radius: 20px;
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.api-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);
}
.api-input-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 15px;
}
.api-input-group label {
font-weight: 600;
color: #d1d5db;
font-size: 14px;
}
.api-input-group input {
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;
width: 100%;
}
.api-input-group input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
}
.api-input-group button {
align-self: flex-start;
}
.api-status {
margin-top: 10px;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.5);
border-radius: 6px;
font-size: 12px;
color: #9ca3af;
}
.select-group {
display: flex;
gap: 20px;
margin-bottom: 25px;
flex-wrap: wrap;
align-items: center;
}
.select-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.select-item label {
font-weight: 600;
color: #d1d5db;
font-size: 14px;
}
.select-item select {
padding: 12px 18px;
border: 2px solid rgba(148, 163, 184, 0.3);
border-radius: 12px;
font-size: 14px;
min-width: 200px;
transition: all 0.3s ease;
background: rgba(15, 23, 42, 0.5);
color: #f1f5f9;
}
.select-item select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
}
.button-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.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-success {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
}
.btn-success:hover {
background: linear-gradient(135deg, #059669 0%, #047857 100%);
transform: translateY(-1px);
}
.btn-warning {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: white;
}
.btn-warning:hover {
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
transform: translateY(-1px);
}
.btn-danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
}
.btn-danger:hover {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
transform: translateY(-1px);
}
.btn-info {
background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
color: white;
}
.btn-info:hover {
background: linear-gradient(135deg, #0891b2 0%, #0e7490 100%);
transform: translateY(-1px);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.main-action {
text-align: center;
margin: 20px 0;
}
.main-action .btn {
font-size: 16px;
padding: 15px 30px;
box-shadow: 0 8px 25px rgba(245, 158, 11, 0.4);
width: 100%;
}
.status-section {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
justify-content: center;
}
.status-indicator {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
background: linear-gradient(135deg, #1e3a5f 0%, #2d4a6b 100%);
border-radius: 25px;
border: 1px solid rgba(0, 255, 255, 0.2);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-dot.running {
background: #ed8936;
box-shadow: 0 0 10px rgba(237, 137, 54, 0.5);
}
.status-dot.idle {
background: #48bb78;
box-shadow: 0 0 10px rgba(72, 187, 120, 0.5);
}
@keyframes pulse {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.1); }
100% { opacity: 1; transform: scale(1); }
}
.output-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
color: #22c55e;
}
.output-title {
font-size: 1.1em;
font-weight: 600;
text-shadow: 0 0 10px rgba(34, 197, 94, 0.3);
}
.output-header .status-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: rgba(0, 0, 0, 0.3);
border-radius: 15px;
border: 1px solid rgba(34, 197, 94, 0.4);
font-size: 12px;
}
.output-header .status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.output-header .status-dot.running {
background: #f59e0b;
box-shadow: 0 0 8px rgba(245, 158, 11, 0.6);
}
.output-header .status-dot.idle {
background: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
}
.output-controls {
display: flex;
gap: 10px;
}
.output-btn {
background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
color: #22c55e;
border: 1px solid rgba(34, 197, 94, 0.4);
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s ease;
}
.output-btn:hover {
background: linear-gradient(135deg, #374151 0%, #1f2937 100%);
box-shadow: 0 0 10px rgba(34, 197, 94, 0.3);
}
.output-content {
background: #000;
border-radius: 10px;
padding: 15px;
height: calc(100% - 60px);
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
color: #22c55e;
white-space: pre-line;
border: 1px solid rgba(34, 197, 94, 0.2);
word-break: break-all;
word-wrap: break-word;
}
.function-group {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
padding: 30px;
border-radius: 20px;
margin-bottom: 30px;
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.function-group h4 {
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;
}
.function-group h4::before {
content: '🔧';
font-size: 1.2em;
}
/* SDM 加密區塊特殊圖示 */
.function-group:first-child h4::before {
content: '🔐';
}
/* 無 SDM 加密區塊特殊圖示 */
.function-group:nth-child(2) h4::before {
content: '📄';
}
.function-group .button-group {
margin-bottom: 0;
gap: 12px;
}
.function-group .btn {
padding: 12px 25px;
font-size: 14px;
min-width: 140px;
}
@media (max-width: 1200px) {
.main-layout {
grid-template-columns: 1fr;
gap: 20px;
}
.control-panel {
height: auto;
}
}
@media (max-width: 900px) {
.select-group {
flex-direction: column !important;
gap: 10px !important;
}
}
@media (max-width: 768px) {
.container {
margin: 10px;
border-radius: 15px;
}
.header h1 {
font-size: 2em;
}
.main-layout {
grid-template-columns: 1fr;
}
.select-group {
flex-direction: column;
align-items: stretch;
}
.button-group {
flex-direction: column;
}
.btn {
width: 100%;
}
.api-input-group {
flex-direction: column;
}
.api-input-group input {
min-width: auto;
}
.output-controls {
flex-wrap: wrap;
gap: 8px;
}
.output-btn {
font-size: 11px;
padding: 4px 8px;
}
}
/* 浮動視窗樣式 */
.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[style*="min-width: 600px"] {
min-width: 600px;
max-width: 800px;
max-height: 80vh;
}
.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-input-group textarea {
width: 100%;
padding: 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;
font-family: 'Courier New', monospace;
resize: vertical;
line-height: 1.4;
}
.modal-input-group textarea: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;
}
</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 active">主頁面</a>
<a href="/manual_test" class="nav-link">手動測試</a>
</div>
<div class="content">
<!-- 選擇區域 -->
<div class="top-section">
<h3>🎯 選擇設定</h3>
<div class="select-group">
<div class="select-item">
<label for="key-select">選擇 Key</label>
<select id="key-select"></select>
</div>
<div class="select-item">
<label for="url-select">選擇 URL</label>
<div style="display: flex; flex-direction: row; gap: 12px; align-items: center;">
<select id="url-select"></select>
<button class="btn btn-secondary" id="edit-url-btn" style="padding: 10px 15px; font-size: 12px;">✏️ 編輯</button>
</div>
</div>
<div class="select-item" style="display: flex; flex-direction: column;">
<label for="name-select">選擇NFC名稱</label>
<div style="display: flex; flex-direction: row; gap: 12px; align-items: center;">
<div class="custom-dropdown" style="position: relative; display: inline-block; min-width: 120px;">
<div class="dropdown-header" id="dropdown-header" style="padding: 12px 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; cursor: pointer; display: flex; justify-content: space-between; align-items: center;">
<span id="selected-name">請選擇NFC名稱</span>
<span style="font-size: 12px;"></span>
</div>
<div class="dropdown-content" id="dropdown-content" style="display: none; position: absolute; top: 100%; left: 0; right: 0; background: rgba(15, 23, 42, 0.95); border: 1px solid rgba(148, 163, 184, 0.2); border-radius: 12px; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4); z-index: 1000; max-height: 200px; overflow-y: auto;">
<!-- 選項將在這裡動態生成 -->
</div>
</div>
<button class="btn btn-primary" id="add-name-btn" type="button">新增</button>
</div>
</div>
</div>
</div>
<!-- 主要佈局:執行結果 + 控制面板 -->
<div class="main-layout">
<!-- 執行結果 -->
<div class="output-section">
<div class="output-header">
<div class="output-title">📊 執行結果</div>
<div class="status-indicator">
<div class="status-dot idle" id="statusDot"></div>
<span id="statusText">閒置</span>
</div>
<div class="status-indicator" id="wsStatusIndicator" style="margin-left: 10px;">
<div class="status-dot idle" id="wsStatusDot"></div>
<span id="wsStatusText">WebSocket 未連接</span>
</div>
<div class="output-controls">
<button class="output-btn" onclick="scrollToBottom()">⬇️ 到底部</button>
<button class="output-btn" onclick="scrollToTop()">⬆️ 到頂部</button>
<button class="output-btn" onclick="clearOutput()">🗑️ 清除結果</button>
</div>
</div>
<div class="output-content" id="output"></div>
</div>
<!-- 控制面板 -->
<div class="control-panel">
<h3>🎮 主要操作</h3>
<!-- SDM 加密區塊 -->
<div class="function-group">
<h4>🔐 SDM 加密</h4>
<div class="button-group">
<button class="btn btn-success" id="run-btn">📝 生產新SDM</button>
<button class="btn btn-primary" id="check-nfc-btn">🔍 檢驗NFC</button>
<button class="btn btn-warning" id="readall-btn">📖 讀取UID</button>
<button class="btn btn-secondary" id="verify-btn">🔐 CMAC認證</button>
<button class="btn btn-info" id="test-nfc-btn">🧪 完整測試NFC</button>
</div>
</div>
<!-- 無 SDM 加密區塊 -->
<div class="function-group">
<h4>📄 無 SDM 加密</h4>
<div class="button-group">
<button class="btn btn-info" id="writendef-btn">✏️ 寫入 NDEF</button>
<button class="btn btn-info" id="readndef-btn">📖 讀取 NDEF</button>
</div>
</div>
</div>
</div>
<!-- API 設定 -->
<div class="api-section">
<h3>🌐 API 設定</h3>
<div class="api-input-group">
<label for="api-url-input">資料API URL:</label>
<input type="text" id="api-url-input" placeholder="https://tatalotest.gsct.tw/gsct/api/nfc/verify">
<button class="btn btn-secondary" id="set-api-btn">設定資料API URL</button>
</div>
<div class="api-input-group">
<label for="api-token-input">API Token:</label>
<input type="password" id="api-token-input" placeholder="eyJhbGciOiJIUzI1NiJ9...">
<button class="btn btn-secondary" id="set-token-btn">設定 API Token</button>
</div>
<div class="api-status">
<span id="api-status">未設定</span>
</div>
</div>
</div>
</div>
<!-- 新增NFC名稱浮動視窗 -->
<div class="modal-overlay" id="addNameModal">
<div class="modal">
<h3> 新增NFC名稱</h3>
<div class="modal-input-group">
<label for="new-name-input">NFC名稱</label>
<input type="text" id="new-name-input" placeholder="請輸入NFC名稱">
</div>
<div class="modal-buttons">
<button class="btn btn-secondary" id="cancel-add-btn">取消</button>
<button class="btn btn-primary" id="confirm-add-btn">確認新增</button>
</div>
</div>
</div>
<!-- 編輯URL浮動視窗 -->
<div class="modal-overlay" id="editUrlModal">
<div class="modal">
<h3>✏️ 編輯URL</h3>
<div class="modal-input-group">
<label for="url-select-modal">選擇URL編號</label>
<select id="url-select-modal" style="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;">
<option value="">請選擇URL編號</option>
</select>
</div>
<div class="modal-input-group">
<label for="url-content-input">URL內容</label>
<input type="text" id="url-content-input" placeholder="請輸入URL內容">
</div>
<div class="modal-buttons">
<button class="btn btn-secondary" id="cancel-edit-url-btn">取消</button>
<button class="btn btn-danger" id="delete-url-btn">刪除</button>
<button class="btn btn-primary" id="save-url-btn">儲存</button>
</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: 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; 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>
const keySelect = document.getElementById('key-select');
const urlSelect = document.getElementById('url-select');
const outputDiv = document.getElementById('output');
const runBtn = document.getElementById('run-btn');
const readAllBtn = document.getElementById('readall-btn');
const reloadBtn = document.getElementById('reload-btn');
const clearBtn = document.getElementById('clear-btn');
const verifyBtn = document.getElementById('verify-btn');
const checkNfcBtn = document.getElementById('check-nfc-btn');
const testNfcBtn = document.getElementById('test-nfc-btn');
const apiUrlInput = document.getElementById('api-url-input');
const setApiBtn = document.getElementById('set-api-btn');
const apiTokenInput = document.getElementById('api-token-input');
const setTokenBtn = document.getElementById('set-token-btn');
const apiStatus = document.getElementById('api-status');
const dropdownHeader = document.getElementById('dropdown-header');
const selectedNameSpan = document.getElementById('selected-name');
const dropdownContent = document.getElementById('dropdown-content');
const addNameBtn = document.getElementById('add-name-btn');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
// 浮動視窗相關元素
const addNameModal = document.getElementById('addNameModal');
const newNameInput = document.getElementById('new-name-input');
const cancelAddBtn = document.getElementById('cancel-add-btn');
const confirmAddBtn = document.getElementById('confirm-add-btn');
// 編輯URL相關元素
const editUrlBtn = document.getElementById('edit-url-btn');
const editUrlModal = document.getElementById('editUrlModal');
const urlSelectModal = document.getElementById('url-select-modal');
const urlContentInput = document.getElementById('url-content-input');
const cancelEditUrlBtn = document.getElementById('cancel-edit-url-btn');
const deleteUrlBtn = document.getElementById('delete-url-btn');
const saveUrlBtn = document.getElementById('save-url-btn');
// 文字編輯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');
let lastUid = '';
let lastSdm = '';
let lastKey = '';
let lastName = '';
let currentNames = [];
function loadKeys() {
fetch('/api/keys').then(r=>r.json()).then(data=>{
if(data.success && data.keys.length > 0){
keySelect.innerHTML = data.keys.map((k, i)=>`<option value="${i+1}">${k}</option>`).join('');
}else{
keySelect.innerHTML = '<option>無資料</option>';
}
});
}
function loadUrls() {
fetch('/api/urls').then(r=>r.json()).then(data=>{
if(data.success && data.urls.length > 0){
urlSelect.innerHTML = data.urls.map((u, i)=>{
// 限制URL顯示長度為100字元
const displayText = u.length > 100 ? u.substring(0, 100) + '...' : u;
return `<option value="${i+1}">${i+1}. ${displayText}</option>`;
}).join('');
}else{
urlSelect.innerHTML = '<option>無資料</option>';
}
});
}
function loadNames() {
fetch('/api/get_names').then(r=>r.json()).then(data=>{
currentNames = data.names || [];
// 清空並重新填充自定義下拉式選單
dropdownContent.innerHTML = '';
// 添加空白選項
const blankOption = document.createElement('div');
blankOption.className = 'dropdown-option';
blankOption.style.padding = '10px 15px';
blankOption.style.borderBottom = '1px solid rgba(148, 163, 184, 0.2)';
blankOption.style.cursor = 'pointer';
blankOption.style.display = 'flex';
blankOption.style.justifyContent = 'space-between';
blankOption.style.alignItems = 'center';
blankOption.setAttribute('data-name', '');
const blankSpan = document.createElement('span');
blankSpan.textContent = '(空白)';
blankSpan.style.flex = '1';
blankSpan.style.color = '#888';
blankSpan.style.fontStyle = 'italic';
blankOption.appendChild(blankSpan);
dropdownContent.appendChild(blankOption);
// 空白選項點擊事件
blankOption.onclick = () => {
selectedNameSpan.textContent = '請選擇NFC名稱';
dropdownContent.style.display = 'none';
fetch('/api/set_name', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: ''})
});
};
// 空白選項懸停效果
blankOption.onmouseenter = () => {
blankOption.style.backgroundColor = 'rgba(96, 165, 250, 0.1)';
};
blankOption.onmouseleave = () => {
blankOption.style.backgroundColor = 'transparent';
};
if (currentNames.length === 0) {
const emptyOption = document.createElement('div');
emptyOption.style.padding = '10px 15px';
emptyOption.style.color = '#888';
emptyOption.style.fontStyle = 'italic';
emptyOption.textContent = '暫無NFC名稱';
dropdownContent.appendChild(emptyOption);
} else {
currentNames.forEach(name => {
const option = document.createElement('div');
option.className = 'dropdown-option';
option.style.padding = '10px 15px';
option.style.borderBottom = '1px solid rgba(148, 163, 184, 0.2)';
option.style.cursor = 'pointer';
option.style.display = 'flex';
option.style.justifyContent = 'space-between';
option.style.alignItems = 'center';
option.setAttribute('data-name', name);
const nameSpan = document.createElement('span');
nameSpan.textContent = name;
nameSpan.style.flex = '1';
const deleteBtn = document.createElement('button');
deleteBtn.innerHTML = '❌';
deleteBtn.style.background = 'none';
deleteBtn.style.border = 'none';
deleteBtn.style.color = '#ff4444';
deleteBtn.style.cursor = 'pointer';
deleteBtn.style.padding = '2px 6px';
deleteBtn.style.borderRadius = '3px';
deleteBtn.style.fontSize = '12px';
deleteBtn.title = '刪除';
// 刪除按鈕事件
deleteBtn.onclick = (e) => {
e.stopPropagation(); // 防止觸發選項點擊
if(confirm(`確定要刪除「${name}」嗎?`)) {
fetch('/api/delete_name', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name})
}).then(r=>r.json()).then(data=>{
if(data.success) {
loadNames();
// 如果刪除的是當前選中的項目,清空選擇
if(selectedNameSpan.textContent === name) {
selectedNameSpan.textContent = '請選擇NFC名稱';
fetch('/api/set_name', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: ''})
});
}
} else {
alert('刪除失敗: ' + data.message);
}
});
}
};
option.appendChild(nameSpan);
option.appendChild(deleteBtn);
dropdownContent.appendChild(option);
// 選項點擊事件
option.onclick = () => {
selectedNameSpan.textContent = name;
dropdownContent.style.display = 'none';
fetch('/api/set_name', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name})
});
};
// 選項懸停效果
option.onmouseenter = () => {
option.style.backgroundColor = 'rgba(96, 165, 250, 0.1)';
};
option.onmouseleave = () => {
option.style.backgroundColor = 'transparent';
};
});
}
// 設定當前選中的名稱 - 預設為空白
selectedNameSpan.textContent = '請選擇NFC名稱';
});
}
function setStatus(isRunning) {
if (isRunning) {
statusText.textContent = '執行中';
statusDot.className = 'status-dot running';
runBtn.disabled = true;
readAllBtn.disabled = true;
verifyBtn.disabled = true;
} else {
statusText.textContent = '閒置';
statusDot.className = 'status-dot idle';
runBtn.disabled = false;
readAllBtn.disabled = false;
verifyBtn.disabled = false;
}
}
function scrollToBottom() {
outputDiv.scrollTop = outputDiv.scrollHeight;
}
// 添加帶顏色的輸出行
function addColoredLine(text) {
// 創建一個臨時 div 來處理 ANSI 顏色代碼
const tempDiv = document.createElement('div');
tempDiv.innerHTML = parseAnsiColors(text);
outputDiv.appendChild(tempDiv);
}
// 解析 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 scrollToTop() {
outputDiv.scrollTop = 0;
}
// 顯示新增名稱浮動視窗
function showAddNameModal() {
addNameModal.style.display = 'flex';
newNameInput.value = '';
newNameInput.focus();
}
// 隱藏新增名稱浮動視窗
function hideAddNameModal() {
addNameModal.style.display = 'none';
newNameInput.value = '';
}
// 顯示編輯URL浮動視窗
function showEditUrlModal() {
editUrlModal.style.display = 'flex';
loadUrlsForModal();
urlSelectModal.value = '';
urlContentInput.value = '';
}
// 隱藏編輯URL浮動視窗
function hideEditUrlModal() {
editUrlModal.style.display = 'none';
urlSelectModal.value = '';
urlContentInput.value = '';
}
// 載入URL選項到編輯視窗
function loadUrlsForModal() {
fetch('/api/urls').then(r=>r.json()).then(data=>{
if(data.success && data.urls.length > 0){
urlSelectModal.innerHTML = '<option value="">請選擇URL編號</option>' +
data.urls.map((url, i) => {
// 顯示URL的前100個字元如果超過則加上...
const displayText = url.length > 100 ? url.substring(0, 100) + '...' : url;
return `<option value="${i+1}">${i+1}. ${displayText}</option>`;
}).join('');
} else {
urlSelectModal.innerHTML = '<option value="">無URL資料</option>';
}
});
}
// 顯示文字編輯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 = '';
}
});
}
// 初始載入
loadKeys();
loadUrls();
loadNames(); // 新增 loadNames 呼叫
setStatus(false);
// 載入 API URL
fetch('/api/get_api_url').then(r=>r.json()).then(data=>{
apiUrlInput.value = data.api_url || '';
});
// 載入自定義名稱
// fetch('/api/get_name').then(r=>r.json()).then(data=>{
// nameInput.value = data.name || '';
// });
// 設定 API URL
setApiBtn.onclick = () => {
const apiUrl = apiUrlInput.value.trim();
if(!apiUrl){
alert('請輸入資料API URL');
return;
}
fetch('/api/set_api_url', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({api_url: apiUrl})
}).then(r=>r.json()).then(data=>{
if(data.success){
alert('資料API URL 設定成功');
updateApiStatus();
}else{
alert('設定失敗: ' + data.message);
}
});
};
// 設定 API Token
setTokenBtn.onclick = () => {
const apiToken = apiTokenInput.value.trim();
if(!apiToken){
alert('請輸入 API Token');
return;
}
fetch('/api/set_api_token', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({api_token: apiToken})
}).then(r=>r.json()).then(data=>{
if(data.success){
alert('API Token 設定成功');
updateApiStatus();
}else{
alert('設定失敗: ' + data.message);
}
});
};
// 更新 API 狀態顯示
function updateApiStatus() {
Promise.all([
fetch('/api/get_api_url').then(r=>r.json()),
fetch('/api/get_api_token').then(r=>r.json())
]).then(([urlData, tokenData]) => {
const hasUrl = urlData.api_url && urlData.api_url.trim() !== '';
const hasToken = tokenData.api_token && tokenData.api_token.trim() !== '';
if (hasUrl && hasToken) {
apiStatus.textContent = '✅ API 已完整設定';
apiStatus.style.color = '#10b981';
} else if (hasUrl) {
apiStatus.textContent = '⚠️ 缺少 API Token';
apiStatus.style.color = '#f59e0b';
} else if (hasToken) {
apiStatus.textContent = '⚠️ 缺少資料API URL';
apiStatus.style.color = '#f59e0b';
} else {
apiStatus.textContent = '❌ API 未設定';
apiStatus.style.color = '#ef4444';
}
});
}
// 清除輸出函數
function clearOutput() {
outputDiv.textContent = '';
}
// SocketIO
const socket = io();
// WebSocket 連接狀態管理
let isConnected = false;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
// 更新 WebSocket 狀態指示器
function updateWebSocketStatus(connected, message) {
const wsStatusDot = document.getElementById('wsStatusDot');
const wsStatusText = document.getElementById('wsStatusText');
if (connected) {
wsStatusDot.className = 'status-dot idle';
wsStatusText.textContent = 'WebSocket 已連接';
} else {
wsStatusDot.className = 'status-dot running';
wsStatusText.textContent = message || 'WebSocket 未連接';
}
}
// 監聽連接事件
socket.on('connect', () => {
console.log('WebSocket 已連接');
isConnected = true;
reconnectAttempts = 0;
updateWebSocketStatus(true);
addColoredLine('🔗 WebSocket 已連接');
outputDiv.scrollTop = outputDiv.scrollHeight;
});
socket.on('disconnect', () => {
console.log('WebSocket 已斷開');
isConnected = false;
updateWebSocketStatus(false, 'WebSocket 已斷開');
addColoredLine('❌ WebSocket 已斷開');
outputDiv.scrollTop = outputDiv.scrollHeight;
});
socket.on('connect_error', (error) => {
console.error('WebSocket 連接錯誤:', error);
updateWebSocketStatus(false, 'WebSocket 連接錯誤');
addColoredLine(`❌ WebSocket 連接錯誤: ${error.message}`);
outputDiv.scrollTop = outputDiv.scrollHeight;
});
// 監聽 script_output解析 UID/SDM/key
socket.on('script_output', data => {
console.log('收到 script_output:', data);
addColoredLine(data.line);
outputDiv.scrollTop = outputDiv.scrollHeight;
setStatus(true);
// 解析 UID
if(data.line.startsWith('UID:')){
lastUid = data.line.replace('UID:', '').trim();
}
if(data.line.startsWith('SDM 讀取計數器:')){
lastSdm = data.line.replace('SDM 讀取計數器:', '').trim();
}
// 更新lastName為當前選中的名稱
lastName = selectedNameSpan.textContent === '請選擇NFC名稱' ? '' : selectedNameSpan.textContent;
// 解析 oldkey 行號
if(data.line.startsWith('找到可用 oldkey第')){
const m = data.line.match(/找到可用 oldkey第(\d+)行/);
if(m){
const keyIdx = parseInt(m[1], 10) - 1;
// 取得 key 值
if(keySelect.options[keyIdx]){
lastKey = keySelect.options[keyIdx].text;
}
}
}
// 儲存當前名稱
lastName = nameInput.value.trim();
// 若回應 200/400/401/402 或 ID已存在/ID新增成功恢復閒置狀態
if(data.line === '200' || data.line === '400' || data.line === '401' || data.line === '402' ||
data.line.includes('ID已存在') || data.line.includes('ID新增成功') ||
data.line.includes('更新ctr次數') || data.line.includes('更新key及ctr次數')){
setStatus(false);
}
});
socket.on('script_finished', data => {
console.log('收到 script_finished:', data);
addColoredLine(`\n=== ${data.message} ===`);
outputDiv.scrollTop = outputDiv.scrollHeight;
setStatus(false);
});
// 定期檢查連接狀態
setInterval(() => {
if (!isConnected && reconnectAttempts < maxReconnectAttempts) {
console.log('嘗試重新連接 WebSocket...');
reconnectAttempts++;
socket.connect();
}
}, 5000);
// 按鈕事件
runBtn.onclick = () => {
outputDiv.innerHTML = '';
const key = keySelect.value;
const url = urlSelect.value;
const name = selectedNameSpan.textContent === '請選擇NFC名稱' ? '' : selectedNameSpan.textContent;
setStatus(true);
fetch('/api/run', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({key, url, name})
}).then(r=>r.json()).then(data=>{
if(!data.success){
addColoredLine(data.message);
setStatus(false);
}
});
};
readAllBtn.onclick = () => {
outputDiv.innerHTML = '';
setStatus(true);
fetch('/api/read_nfc', {
method: 'POST'
}).then(r=>r.json()).then(data=>{
if(!data.success){
addColoredLine(data.message);
setStatus(false);
}
});
};
verifyBtn.onclick = () => {
outputDiv.innerHTML = '';
setStatus(true);
const key = keySelect.value;
const name = selectedNameSpan.textContent === '請選擇NFC名稱' ? '' : selectedNameSpan.textContent;
fetch('/api/verify', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({key, name})
}).then(r=>r.json()).then(data=>{
if(!data.success){
addColoredLine(data.message);
setStatus(false);
}
});
};
checkNfcBtn.onclick = () => {
outputDiv.innerHTML = '';
setStatus(true);
const key = keySelect.value;
const name = selectedNameSpan.textContent === '請選擇NFC名稱' ? '' : selectedNameSpan.textContent;
fetch('/api/check_nfc', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({key, name})
}).then(r=>r.json()).then(data=>{
if(!data.success){
addColoredLine(data.message);
setStatus(false);
}
});
};
testNfcBtn.onclick = () => {
// 顯示確認對話框
if (confirm('NFC內資料清空會寫入預設資料確認是否正常你還要繼續嗎?')) {
outputDiv.innerHTML = '';
setStatus(true);
fetch('/api/test_nfc', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(r=>r.json()).then(data=>{
if(!data.success){
addColoredLine(data.message);
setStatus(false);
}
});
}
};
addNameBtn.onclick = () => {
showAddNameModal();
};
// 編輯URL按鈕事件
editUrlBtn.onclick = () => {
showTextEditUrlModal();
};
// 寫入 NDEF 按鈕事件
document.getElementById('writendef-btn').onclick = () => {
const urlIndex = urlSelect.value;
if (!urlIndex) {
alert('請先選擇 URL');
return;
}
outputDiv.textContent = '';
setStatus(true);
fetch('/api/manual_writendef', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({url_index: urlIndex})
}).then(r=>r.json()).then(data=>{
if(!data.success){
outputDiv.textContent = data.message;
setStatus(false);
}
});
};
// 讀取 NDEF 按鈕事件
document.getElementById('readndef-btn').onclick = () => {
outputDiv.textContent = '';
setStatus(true);
fetch('/api/manual_readndef', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
}).then(r=>r.json()).then(data=>{
if(!data.success){
outputDiv.textContent = data.message;
setStatus(false);
}
});
};
// 下拉式選單切換
dropdownHeader.onclick = () => {
if (dropdownContent.style.display === 'none' || dropdownContent.style.display === '') {
dropdownContent.style.display = 'block';
} else {
dropdownContent.style.display = 'none';
}
};
// 點擊外部關閉下拉式選單
document.addEventListener('click', (e) => {
if (!dropdownHeader.contains(e.target) && !dropdownContent.contains(e.target)) {
dropdownContent.style.display = 'none';
}
});
// URL選擇變更事件
urlSelectModal.onchange = () => {
const selectedIndex = urlSelectModal.value;
if (selectedIndex) {
fetch('/api/get_url_by_index', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({index: selectedIndex})
}).then(r=>r.json()).then(data=>{
if(data.success) {
urlContentInput.value = data.url;
} else {
alert('載入URL失敗: ' + data.message);
urlContentInput.value = '';
}
});
} else {
urlContentInput.value = '';
}
};
// 取消新增按鈕
cancelAddBtn.onclick = () => {
hideAddNameModal();
};
// 確認新增按鈕
confirmAddBtn.onclick = () => {
const name = newNameInput.value.trim();
if(!name) {
alert('請輸入NFC名稱');
return;
}
if(currentNames.includes(name)) {
alert('此名稱已存在');
return;
}
fetch('/api/add_name', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name})
}).then(r=>r.json()).then(data=>{
if(data.success) {
hideAddNameModal();
loadNames();
// 不自動選中新新增的名稱,保持空白
} else {
alert('新增失敗: ' + data.message);
}
});
};
// 按ESC鍵關閉浮動視窗
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && addNameModal.style.display === 'flex') {
hideAddNameModal();
}
if (e.key === 'Escape' && editUrlModal.style.display === 'flex') {
hideEditUrlModal();
}
if (e.key === 'Escape' && textEditUrlModal.style.display === 'flex') {
hideTextEditUrlModal();
}
});
// 點擊浮動視窗背景關閉浮動視窗
addNameModal.addEventListener('click', (e) => {
if (e.target === addNameModal) {
hideAddNameModal();
}
});
editUrlModal.addEventListener('click', (e) => {
if (e.target === editUrlModal) {
hideEditUrlModal();
}
});
textEditUrlModal.addEventListener('click', (e) => {
if (e.target === textEditUrlModal) {
hideTextEditUrlModal();
}
});
// 編輯URL視窗按鈕事件
cancelEditUrlBtn.onclick = () => {
hideEditUrlModal();
};
deleteUrlBtn.onclick = () => {
const selectedIndex = urlSelectModal.value;
if (!selectedIndex) {
alert('請先選擇要刪除的URL');
return;
}
if (confirm('確定要刪除此URL嗎')) {
fetch('/api/delete_url_by_index', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({index: selectedIndex})
}).then(r=>r.json()).then(data=>{
if(data.success) {
alert('URL刪除成功');
hideEditUrlModal();
loadUrls(); // 重新載入URL選項
} else {
alert('刪除失敗: ' + data.message);
}
});
}
};
saveUrlBtn.onclick = () => {
const selectedIndex = urlSelectModal.value;
const newUrl = urlContentInput.value.trim();
if (!selectedIndex) {
alert('請先選擇要編輯的URL');
return;
}
if (!newUrl) {
alert('URL內容不得為空');
return;
}
fetch('/api/update_url_by_index', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({index: selectedIndex, url: newUrl})
}).then(r=>r.json()).then(data=>{
if(data.success) {
alert('URL更新成功');
hideEditUrlModal();
loadUrls(); // 重新載入URL選項
} else {
alert('更新失敗: ' + data.message);
}
});
};
// 文字編輯URL視窗按鈕事件
cancelTextEditBtn.onclick = () => {
hideTextEditUrlModal();
};
saveTextEditBtn.onclick = () => {
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) {
alert(`URLs文件保存成功共保存了 ${data.urls_count} 個URL`);
hideTextEditUrlModal();
loadUrls(); // 重新載入URL選項
} else {
alert('保存失敗: ' + data.message);
}
});
} else {
alert('無法讀取現有URLs請重試');
}
});
};
// 移除 deleteNameBtn, 相關事件
// 頁面載入時初始化
document.addEventListener('DOMContentLoaded', function() {
// 載入 API 設定
loadApiSettings();
// 更新 API 狀態
updateApiStatus();
});
// 載入 API 設定
function loadApiSettings() {
// 載入 API URL
fetch('/api/get_api_url').then(r=>r.json()).then(data=>{
if(data.api_url) {
apiUrlInput.value = data.api_url;
}
});
// 載入 API Token
fetch('/api/get_api_token').then(r=>r.json()).then(data=>{
if(data.api_token) {
apiTokenInput.value = data.api_token;
}
});
}
</script>
</body>
</html>