- 新增 /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>
126 lines
3.8 KiB
JavaScript
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 });
|
|
}
|