Files
rental-contract-pdf/public/admin.js
JianMiau 89a4f891c2 新增管理員頁面、Word PDF 預覽、租賃日期欄位、SSL docker-compose
- 新增 /admin.html:上傳/刪除範本,HTTP Basic Auth 保護
- Word 預覽改用 LibreOffice PDF 轉換,帶入表單參數即時顯示
- 新增租賃開始/結束年月日、租期年數佔位符支援
- 預覽 loading 遮罩,修正 hidden 被 CSS display:flex 覆蓋的問題
- 左右欄 UI 重構,右欄固定顯示 Word 預覽
- 新增 docker-compose.yml + nginx SSL reverse proxy
- admin 密碼改由 ADMIN_PASSWORD 環境變數設定

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 23:02:33 +08:00

126 lines
3.8 KiB
JavaScript

let adminPassword = sessionStorage.getItem('adminPassword');
const loginSection = document.getElementById('loginSection');
const adminSection = document.getElementById('adminSection');
const loginError = document.getElementById('loginError');
const uploadMessage = document.getElementById('uploadMessage');
if (adminPassword) {
checkAuth();
} else {
showLogin();
}
document.getElementById('loginButton').addEventListener('click', () => {
adminPassword = document.getElementById('passwordInput').value;
checkAuth();
});
document.getElementById('passwordInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') document.getElementById('loginButton').click();
});
document.getElementById('uploadButton').addEventListener('click', async () => {
const file = document.getElementById('uploadInput').files[0];
uploadMessage.textContent = '';
if (!file) { uploadMessage.textContent = '請選擇檔案。'; return; }
const formData = new FormData();
formData.append('template', file);
const res = await fetchAdmin('/api/admin/templates', { method: 'POST', body: formData });
const body = await res.json().catch(() => ({}));
if (res.ok) {
uploadMessage.textContent = `已上傳:${body.name}`;
document.getElementById('uploadInput').value = '';
loadTemplates();
} else {
uploadMessage.textContent = body.error || '上傳失敗。';
}
});
async function checkAuth() {
loginError.textContent = '';
try {
const res = await fetchAdmin('/api/admin/check');
if (res.ok) {
sessionStorage.setItem('adminPassword', adminPassword);
showAdmin();
} else {
loginError.textContent = '密碼錯誤。';
adminPassword = null;
sessionStorage.removeItem('adminPassword');
showLogin();
}
} catch {
loginError.textContent = '連線失敗。';
}
}
function showLogin() {
loginSection.hidden = false;
adminSection.hidden = true;
}
function showAdmin() {
loginSection.hidden = true;
adminSection.hidden = false;
loadTemplates();
}
async function loadTemplates() {
const list = document.getElementById('templateList');
try {
const res = await fetch('/api/templates');
const { templates } = await res.json();
if (templates.length === 0) {
list.innerHTML = '<p class="message">尚無範本。</p>';
return;
}
list.replaceChildren(...templates.map((name) => {
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--border)';
const label = document.createElement('span');
label.textContent = name;
label.style.flex = '1';
label.style.fontSize = '0.9rem';
const btn = document.createElement('button');
btn.className = 'secondary-button';
btn.textContent = '刪除';
btn.style.minHeight = '34px';
btn.style.padding = '0 14px';
btn.addEventListener('click', () => deleteTemplate(name, row));
row.append(label, btn);
return row;
}));
} catch {
list.innerHTML = '<p class="message">載入失敗。</p>';
}
}
async function deleteTemplate(name, row) {
if (!confirm(`確定要刪除「${name}」?此動作無法復原。`)) return;
const res = await fetchAdmin(`/api/admin/templates/${encodeURIComponent(name)}`, { method: 'DELETE' });
if (res.ok) {
row.remove();
if (!document.getElementById('templateList').children.length) {
document.getElementById('templateList').innerHTML = '<p class="message">尚無範本。</p>';
}
} else {
const body = await res.json().catch(() => ({}));
alert(body.error || '刪除失敗。');
}
}
function fetchAdmin(url, options = {}) {
const headers = new Headers(options.headers);
headers.set('Authorization', `Basic ${btoa('admin:' + adminPassword)}`);
return fetch(url, { ...options, headers });
}