813 lines
28 KiB
HTML
813 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>License Management System</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.header {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.header h1 {
|
|
color: #333;
|
|
display: inline-block;
|
|
}
|
|
|
|
.user-info {
|
|
float: right;
|
|
line-height: 32px;
|
|
}
|
|
|
|
.btn-logout {
|
|
background: #e74c3c;
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
margin-left: 10px;
|
|
}
|
|
|
|
.btn-logout:hover {
|
|
background: #c0392b;
|
|
}
|
|
|
|
.login-container {
|
|
max-width: 400px;
|
|
margin: 100px auto;
|
|
background: white;
|
|
padding: 40px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.login-container h2 {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
color: #333;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
color: #555;
|
|
font-weight: 500;
|
|
}
|
|
|
|
input[type="text"],
|
|
input[type="password"],
|
|
input[type="datetime-local"],
|
|
select,
|
|
textarea {
|
|
width: 100%;
|
|
padding: 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.btn {
|
|
width: 100%;
|
|
padding: 12px;
|
|
background: #667eea;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
transition: background 0.3s;
|
|
}
|
|
|
|
.btn:hover {
|
|
background: #5568d3;
|
|
}
|
|
|
|
.btn:disabled {
|
|
background: #ccc;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.dashboard {
|
|
display: none;
|
|
}
|
|
|
|
.dashboard.active {
|
|
display: block;
|
|
}
|
|
|
|
.tabs {
|
|
background: white;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.tab-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
border-bottom: 2px solid #eee;
|
|
}
|
|
|
|
.tab-btn {
|
|
padding: 12px 24px;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
color: #666;
|
|
border-bottom: 3px solid transparent;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.tab-btn:hover {
|
|
color: #667eea;
|
|
}
|
|
|
|
.tab-btn.active {
|
|
color: #667eea;
|
|
border-bottom-color: #667eea;
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.card {
|
|
background: white;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.card h3 {
|
|
margin-bottom: 20px;
|
|
color: #333;
|
|
}
|
|
|
|
.device-item,
|
|
.request-item {
|
|
background: #f8f9fa;
|
|
padding: 15px;
|
|
margin-bottom: 10px;
|
|
border-radius: 5px;
|
|
border-left: 4px solid #667eea;
|
|
}
|
|
|
|
.device-item.expired {
|
|
border-left-color: #e74c3c;
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 6px 12px;
|
|
font-size: 14px;
|
|
width: auto;
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: #e74c3c;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: #c0392b;
|
|
}
|
|
|
|
.btn-success {
|
|
background: #27ae60;
|
|
}
|
|
|
|
.btn-success:hover {
|
|
background: #229954;
|
|
}
|
|
|
|
.alert {
|
|
padding: 15px;
|
|
margin-bottom: 20px;
|
|
border-radius: 5px;
|
|
display: none;
|
|
}
|
|
|
|
.alert.show {
|
|
display: block;
|
|
}
|
|
|
|
.alert-success {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
border: 1px solid #c3e6cb;
|
|
}
|
|
|
|
.alert-error {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
|
|
.form-row {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-row .form-group {
|
|
flex: 1;
|
|
}
|
|
|
|
code {
|
|
background: #f4f4f4;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
|
|
.auto-renew-switch {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 60px;
|
|
height: 34px;
|
|
}
|
|
|
|
.switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: #ccc;
|
|
transition: .4s;
|
|
border-radius: 34px;
|
|
}
|
|
|
|
.slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 26px;
|
|
width: 26px;
|
|
left: 4px;
|
|
bottom: 4px;
|
|
background-color: white;
|
|
transition: .4s;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
input:checked + .slider {
|
|
background-color: #667eea;
|
|
}
|
|
|
|
input:checked + .slider:before {
|
|
transform: translateX(26px);
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.badge-success {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.badge-danger {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.badge-warning {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Login Screen -->
|
|
<div id="loginScreen" class="login-container">
|
|
<h2>🔐 License Management</h2>
|
|
<div id="loginAlert" class="alert"></div>
|
|
<form id="loginForm">
|
|
<div class="form-group">
|
|
<label>用户名</label>
|
|
<input type="text" id="username" required placeholder="admin">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>密码</label>
|
|
<input type="password" id="password" required placeholder="••••••••">
|
|
</div>
|
|
<button type="submit" class="btn">登录</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Dashboard -->
|
|
<div id="dashboard" class="dashboard">
|
|
<div class="header">
|
|
<h1>📋 License Management System</h1>
|
|
<div class="user-info">
|
|
<span>欢迎, <strong id="currentUser"></strong></span>
|
|
<button class="btn-logout" onclick="logout()">退出</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div id="dashboardAlert" class="alert"></div>
|
|
|
|
<div class="tabs">
|
|
<div class="tab-buttons">
|
|
<button class="tab-btn active" onclick="switchTab('devices')">设备管理</button>
|
|
<button class="tab-btn" onclick="switchTab('sign')">签发License</button>
|
|
<button class="tab-btn" onclick="switchTab('requests')">续期审批</button>
|
|
<button class="tab-btn" onclick="switchTab('settings')">系统设置</button>
|
|
</div>
|
|
|
|
<!-- Devices Tab -->
|
|
<div id="devices" class="tab-content active">
|
|
<div class="card">
|
|
<h3>设备列表</h3>
|
|
<button class="btn btn-small" onclick="loadDevices()">刷新列表</button>
|
|
<div id="deviceList"></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>创建/更新设备</h3>
|
|
<form id="deviceForm">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>设备ID</label>
|
|
<input type="text" id="deviceId" required placeholder="dev-001">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>到期时间</label>
|
|
<input type="datetime-local" id="expiration" required>
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="btn">保存设备</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sign License Tab -->
|
|
<div id="sign" class="tab-content">
|
|
<div class="card">
|
|
<h3>签发License</h3>
|
|
<form id="signForm">
|
|
<div class="form-group">
|
|
<label>设备ID</label>
|
|
<input type="text" id="signDeviceId" required placeholder="dev-001">
|
|
</div>
|
|
<button type="submit" class="btn">签发License</button>
|
|
</form>
|
|
<div id="licenseResult" style="margin-top: 20px;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Requests Tab -->
|
|
<div id="requests" class="tab-content">
|
|
<div class="card">
|
|
<h3>待审批的续期请求</h3>
|
|
<button class="btn btn-small" onclick="loadPendingRequests()">刷新列表</button>
|
|
<div id="requestList"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Tab -->
|
|
<div id="settings" class="tab-content">
|
|
<div class="card">
|
|
<h3>系统设置</h3>
|
|
<div class="auto-renew-switch">
|
|
<span>自动续期所有设备:</span>
|
|
<label class="switch">
|
|
<input type="checkbox" id="autoRenewSwitch" onchange="toggleAutoRenew()">
|
|
<span class="slider"></span>
|
|
</label>
|
|
<span id="autoRenewStatus" class="badge badge-warning">关闭</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>🔑 Ed25519 公钥</h3>
|
|
<p style="margin-bottom: 15px; color: #666;">
|
|
此公钥用于验证 License 签名的有效性。您可以安全地将此公钥分发给需要验证 License 的设备。
|
|
</p>
|
|
<button class="btn btn-small" onclick="loadPublicKey()">刷新公钥</button>
|
|
<div id="publicKeyResult" style="margin-top: 20px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = '/api';
|
|
let currentToken = '';
|
|
let currentUsername = '';
|
|
|
|
// Tab switching
|
|
function switchTab(tabName) {
|
|
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
|
|
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
|
document.getElementById(tabName).classList.add('active');
|
|
event.target.classList.add('active');
|
|
|
|
if (tabName === 'devices') {
|
|
loadDevices();
|
|
} else if (tabName === 'requests') {
|
|
loadPendingRequests();
|
|
}
|
|
}
|
|
|
|
// Show alert
|
|
function showAlert(elementId, message, type = 'success') {
|
|
const alert = document.getElementById(elementId);
|
|
alert.className = `alert alert-${type} show`;
|
|
alert.textContent = message;
|
|
setTimeout(() => alert.classList.remove('show'), 3000);
|
|
}
|
|
|
|
// Login
|
|
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const username = document.getElementById('username').value;
|
|
const password = document.getElementById('password').value;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (response.ok) {
|
|
currentToken = data.token;
|
|
currentUsername = username;
|
|
document.getElementById('currentUser').textContent = username;
|
|
document.getElementById('loginScreen').style.display = 'none';
|
|
document.getElementById('dashboard').classList.add('active');
|
|
loadDevices();
|
|
loadAutoRenewStatus();
|
|
} else {
|
|
showAlert('loginAlert', data.message || '登录失败', 'error');
|
|
}
|
|
} catch (error) {
|
|
showAlert('loginAlert', '网络错误: ' + error.message, 'error');
|
|
}
|
|
});
|
|
|
|
// Logout
|
|
function logout() {
|
|
currentToken = '';
|
|
currentUsername = '';
|
|
document.getElementById('loginScreen').style.display = 'block';
|
|
document.getElementById('dashboard').classList.remove('active');
|
|
}
|
|
|
|
// Load devices
|
|
async function loadDevices() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/device/list`, {
|
|
headers: { 'Authorization': `Bearer ${currentToken}` }
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('设备列表加载失败:', response.status, errorText);
|
|
showAlert('dashboardAlert', `加载设备列表失败: ${errorText}`, 'error');
|
|
return;
|
|
}
|
|
|
|
const devices = await response.json();
|
|
const listDiv = document.getElementById('deviceList');
|
|
|
|
// 确保 devices 是数组
|
|
if (!Array.isArray(devices)) {
|
|
console.error('设备列表数据格式错误:', typeof devices, devices);
|
|
listDiv.innerHTML = `<p style="color: #e74c3c;">数据格式错误: ${typeof devices}</p>`;
|
|
return;
|
|
}
|
|
|
|
if (devices.length === 0) {
|
|
listDiv.innerHTML = '<p>暂无设备</p>';
|
|
return;
|
|
}
|
|
|
|
listDiv.innerHTML = devices.map(device => {
|
|
const expDate = new Date(device.expiration);
|
|
const isExpired = expDate < new Date();
|
|
return `
|
|
<div class="device-item ${isExpired ? 'expired' : ''}">
|
|
<strong>${device.device_id}</strong> -
|
|
到期: ${expDate.toLocaleString('zh-CN')}
|
|
${isExpired ? '<span class="badge badge-danger">已过期</span>' : '<span class="badge badge-success">有效</span>'}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
} catch (error) {
|
|
console.error('加载设备失败:', error);
|
|
showAlert('dashboardAlert', '加载设备失败: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Create/Update device
|
|
document.getElementById('deviceForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const deviceId = document.getElementById('deviceId').value;
|
|
const expiration = document.getElementById('expiration').value;
|
|
|
|
// Convert local datetime to RFC3339
|
|
const expirationISO = new Date(expiration).toISOString();
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/device/manage`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${currentToken}`
|
|
},
|
|
body: JSON.stringify({ device_id: deviceId, expiration: expirationISO })
|
|
});
|
|
|
|
const text = await response.text();
|
|
if (response.ok) {
|
|
showAlert('dashboardAlert', text, 'success');
|
|
document.getElementById('deviceForm').reset();
|
|
loadDevices();
|
|
} else {
|
|
showAlert('dashboardAlert', text || '操作失败', 'error');
|
|
}
|
|
} catch (error) {
|
|
showAlert('dashboardAlert', '网络错误: ' + error.message, 'error');
|
|
}
|
|
});
|
|
|
|
// Sign license
|
|
document.getElementById('signForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const deviceId = document.getElementById('signDeviceId').value;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/license/sign`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${currentToken}`
|
|
},
|
|
body: JSON.stringify({ device_id: deviceId })
|
|
});
|
|
|
|
const license = await response.json();
|
|
if (response.ok) {
|
|
const resultDiv = document.getElementById('licenseResult');
|
|
resultDiv.innerHTML = `
|
|
<h4>签发成功!</h4>
|
|
<pre style="background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto;">${JSON.stringify(license, null, 2)}</pre>
|
|
`;
|
|
showAlert('dashboardAlert', 'License签发成功', 'success');
|
|
} else {
|
|
showAlert('dashboardAlert', license.message || '签发失败', 'error');
|
|
}
|
|
} catch (error) {
|
|
showAlert('dashboardAlert', '网络错误: ' + error.message, 'error');
|
|
}
|
|
});
|
|
|
|
// Load pending requests
|
|
async function loadPendingRequests() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/admin/pending_requests`, {
|
|
method: 'POST',
|
|
headers: { 'Authorization': `Bearer ${currentToken}` }
|
|
});
|
|
|
|
if (!response.ok) {
|
|
showAlert('dashboardAlert', '加载申请列表失败', 'error');
|
|
return;
|
|
}
|
|
|
|
const requests = await response.json();
|
|
const listDiv = document.getElementById('requestList');
|
|
|
|
// 确保 requests 是数组
|
|
if (!Array.isArray(requests)) {
|
|
listDiv.innerHTML = '<p style="color: #e74c3c;">数据格式错误</p>';
|
|
return;
|
|
}
|
|
|
|
if (requests.length === 0) {
|
|
listDiv.innerHTML = '<p>暂无待审批申请</p>';
|
|
return;
|
|
}
|
|
|
|
listDiv.innerHTML = requests.map(req => {
|
|
const reqDate = new Date(req.request_time);
|
|
return `
|
|
<div class="request-item">
|
|
<strong>设备ID: ${req.device_id}</strong><br>
|
|
申请时间: ${reqDate.toLocaleString('zh-CN')}<br>
|
|
<button class="btn btn-small btn-success" onclick="approveRequest('${req.device_id}')">批准</button>
|
|
<button class="btn btn-small btn-danger" onclick="rejectRequest('${req.device_id}')">拒绝</button>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
} catch (error) {
|
|
showAlert('dashboardAlert', '加载申请失败: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Approve request
|
|
async function approveRequest(deviceId) {
|
|
const expiration = new Date();
|
|
expiration.setFullYear(expiration.getFullYear() + 1);
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/admin/handle_request`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${currentToken}`
|
|
},
|
|
body: JSON.stringify({
|
|
device_id: deviceId,
|
|
expiration: expiration.toISOString(),
|
|
approved_by: currentUsername,
|
|
action: 'approve'
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
showAlert('dashboardAlert', '申请已批准', 'success');
|
|
loadPendingRequests();
|
|
loadDevices();
|
|
} else {
|
|
showAlert('dashboardAlert', '批准失败', 'error');
|
|
}
|
|
} catch (error) {
|
|
showAlert('dashboardAlert', '网络错误: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Reject request
|
|
async function rejectRequest(deviceId) {
|
|
const expiration = new Date();
|
|
expiration.setFullYear(expiration.getFullYear() + 1);
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/admin/handle_request`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${currentToken}`
|
|
},
|
|
body: JSON.stringify({
|
|
device_id: deviceId,
|
|
expiration: expiration.toISOString(),
|
|
approved_by: currentUsername,
|
|
action: 'reject'
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
showAlert('dashboardAlert', '申请已拒绝', 'success');
|
|
loadPendingRequests();
|
|
} else {
|
|
showAlert('dashboardAlert', '拒绝失败', 'error');
|
|
}
|
|
} catch (error) {
|
|
showAlert('dashboardAlert', '网络错误: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Load auto renew status
|
|
async function loadAutoRenewStatus() {
|
|
// This would require a GET endpoint, for now we'll just show the toggle
|
|
}
|
|
|
|
// Toggle auto renew
|
|
async function toggleAutoRenew() {
|
|
const enabled = document.getElementById('autoRenewSwitch').checked;
|
|
const status = document.getElementById('autoRenewStatus');
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/admin/allow_auto_renew`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${currentToken}`
|
|
},
|
|
body: JSON.stringify({ enabled: enabled.toString() })
|
|
});
|
|
|
|
if (response.ok) {
|
|
status.textContent = enabled ? '开启' : '关闭';
|
|
status.className = enabled ? 'badge badge-success' : 'badge badge-warning';
|
|
showAlert('dashboardAlert', enabled ? '自动续期已开启' : '自动续期已关闭', 'success');
|
|
} else {
|
|
document.getElementById('autoRenewSwitch').checked = !enabled;
|
|
showAlert('dashboardAlert', '设置失败', 'error');
|
|
}
|
|
} catch (error) {
|
|
document.getElementById('autoRenewSwitch').checked = !enabled;
|
|
showAlert('dashboardAlert', '网络错误: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Load public key
|
|
async function loadPublicKey() {
|
|
const resultDiv = document.getElementById('publicKeyResult');
|
|
resultDiv.innerHTML = '<p>加载中...</p>';
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/license/public-key`);
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
resultDiv.innerHTML = `
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-bottom: 10px;">SSH 格式 (推荐)</h4>
|
|
<pre style="background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; word-break: break-all;">${data.ssh_format.trim()}</pre>
|
|
</div>
|
|
<div>
|
|
<h4 style="margin-bottom: 10px;">Base64 格式</h4>
|
|
<pre style="background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; word-break: break-all;">${data.base64}</pre>
|
|
</div>
|
|
`;
|
|
} else {
|
|
resultDiv.innerHTML = '<p style="color: #e74c3c;">加载公钥失败</p>';
|
|
}
|
|
} catch (error) {
|
|
resultDiv.innerHTML = `<p style="color: #e74c3c;">网络错误: ${error.message}</p>`;
|
|
}
|
|
}
|
|
|
|
// Set default expiration to 1 year from now
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
const expInput = document.getElementById('expiration');
|
|
const nextYear = new Date();
|
|
nextYear.setFullYear(nextYear.getFullYear() + 1);
|
|
const localDatetime = new Date(nextYear.getTime() - nextYear.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
|
expInput.value = localDatetime;
|
|
});
|
|
|
|
// Auto-load public key when switching to settings tab
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const settingsBtn = document.querySelector('.tab-btn[onclick*="settings"]');
|
|
if (settingsBtn) {
|
|
settingsBtn.addEventListener('click', function() {
|
|
setTimeout(loadPublicKey, 100);
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|