645 lines
28 KiB
HTML
645 lines
28 KiB
HTML
<!doctype html>
|
||
<html lang="zh-Hant">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>管理器 - Dashboard</title>
|
||
<link rel="stylesheet" href="/static/css/my-style.css">
|
||
<style>
|
||
.tabs { display:flex; gap:8px; margin-bottom:12px; }
|
||
.tab { padding:8px 12px; border:1px solid #ddd; cursor:pointer; }
|
||
.active { background:#f0f0f0; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>網站管理器</h1>
|
||
</header>
|
||
|
||
<div class="tabs">
|
||
<div class="tab active" data-tab="overview">概覽</div>
|
||
<div class="tab" data-tab="settings">網站設定</div>
|
||
<div class="tab" data-tab="pages">頁面管理</div>
|
||
<div class="tab" data-tab="media">媒體庫</div>
|
||
</div>
|
||
|
||
<div id="content">
|
||
<div id="overview" class="panel">選擇一個專案在右側編輯。</div>
|
||
|
||
<div id="settings" class="panel" style="display:none">
|
||
<h2>網站設定</h2>
|
||
<form id="settingsForm">
|
||
<label>網站標題<br><input id="siteTitle" type="text"></label><br>
|
||
<label>描述<br><textarea id="siteDesc"></textarea></label><br>
|
||
<button id="saveSettings">儲存設定</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div id="pages" class="panel" style="display:none">
|
||
<h2>頁面管理</h2>
|
||
<div id="pagesTree"></div>
|
||
</div>
|
||
|
||
<div id="media" class="panel" style="display:none">
|
||
<h2>媒體庫</h2>
|
||
<input type="file" id="mediaFile"><button id="uploadBtn">上傳</button>
|
||
<div id="mediaGrid"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.querySelectorAll('.tab').forEach(t=>t.addEventListener('click',e=>{
|
||
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
|
||
t.classList.add('active');
|
||
document.querySelectorAll('.panel').forEach(p=>p.style.display='none');
|
||
document.getElementById(t.dataset.tab).style.display='block';
|
||
}));
|
||
|
||
// Dashboard logic: load settings and media for ?slug=...
|
||
(function(){
|
||
const params = new URLSearchParams(location.search);
|
||
const slug = params.get('slug');
|
||
if(!slug){
|
||
document.getElementById('overview').innerText = '請在網址列加入 ?slug=your-site 以管理專案。';
|
||
return;
|
||
}
|
||
|
||
// load settings
|
||
async function loadSettings(){
|
||
const res = await fetch(`/api/projects/${encodeURIComponent(slug)}/settings`);
|
||
if(!res.ok) return;
|
||
const data = await res.json();
|
||
document.getElementById('siteTitle').value = data.title || '';
|
||
document.getElementById('siteDesc').value = data.description || '';
|
||
}
|
||
|
||
document.getElementById('saveSettings').addEventListener('click', async function(e){
|
||
e.preventDefault();
|
||
const body = {
|
||
title: document.getElementById('siteTitle').value,
|
||
description: document.getElementById('siteDesc').value
|
||
};
|
||
const res = await fetch(`/api/projects/${encodeURIComponent(slug)}/settings`, {
|
||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body)
|
||
});
|
||
if(res.ok) alert('設定已儲存'); else alert('儲存失敗');
|
||
});
|
||
|
||
// media upload
|
||
async function listMedia(){
|
||
const res = await fetch(`/api/projects/${encodeURIComponent(slug)}/media/list`);
|
||
const grid = document.getElementById('mediaGrid');
|
||
grid.innerHTML = '';
|
||
if(!res.ok) return;
|
||
const items = await res.json();
|
||
items.reverse();
|
||
items.forEach(it => {
|
||
const card = document.createElement('div');
|
||
card.className = 'media-card';
|
||
card.style.border = '1px solid #ddd'; card.style.padding='8px'; card.style.display='inline-block'; card.style.margin='6px';
|
||
const img = document.createElement('img');
|
||
img.src = it.thumb || it.url;
|
||
img.style.width = '160px'; img.style.height='auto';
|
||
const info = document.createElement('div');
|
||
info.innerHTML = `<div>${it.original_name || it.filename}</div><div style="font-size:12px;color:#666">${it.uploaded_at||''}</div>`;
|
||
const btnCopy = document.createElement('button'); btnCopy.textContent='複製連結';
|
||
btnCopy.addEventListener('click', ()=>{ navigator.clipboard.writeText(it.url); alert('已複製連結'); });
|
||
const btnDel = document.createElement('button'); btnDel.textContent='刪除'; btnDel.style.marginLeft='6px';
|
||
btnDel.addEventListener('click', async ()=>{
|
||
if(!confirm('確定要刪除此媒體檔?')) return;
|
||
// rel_path expected like media/images/YYYY-MM/filename
|
||
const rel = `media/images/${it.date}/${it.filename}`;
|
||
const del = await fetch(`/api/projects/${encodeURIComponent(slug)}/media/${encodeURIComponent(rel)}`, {method:'DELETE'});
|
||
if(del.ok){ alert('刪除成功'); listMedia(); } else alert('刪除失敗');
|
||
});
|
||
card.appendChild(img); card.appendChild(info); card.appendChild(btnCopy); card.appendChild(btnDel);
|
||
grid.appendChild(card);
|
||
});
|
||
}
|
||
|
||
document.getElementById('uploadBtn').addEventListener('click', async ()=>{
|
||
const fileInput = document.getElementById('mediaFile');
|
||
if(!fileInput.files.length){ alert('請選擇檔案'); return; }
|
||
const fd = new FormData(); fd.append('file', fileInput.files[0]);
|
||
const res = await fetch(`/api/projects/${encodeURIComponent(slug)}/media/upload`, {method:'POST', body: fd});
|
||
if(res.ok){ alert('上傳完成'); fileInput.value=''; listMedia(); } else { alert('上傳失敗'); }
|
||
});
|
||
|
||
// init
|
||
loadSettings(); listMedia();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
<!DOCTYPE html>
|
||
<html lang="zh-TW">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>VvvebJS 網頁管理器</title>
|
||
<meta name="description" content="使用 VvvebJS 管理您的靜態網站專案">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="/static/css/my-style.css">
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ── 頂部導覽 ────────────────────────────────────────────────── -->
|
||
<header class="topbar">
|
||
<div class="topbar-brand">
|
||
<svg class="brand-icon" viewBox="0 0 32 32" fill="none">
|
||
<rect width="32" height="32" rx="8" fill="url(#grad)"/>
|
||
<path d="M8 10h16M8 16h10M8 22h13" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
|
||
<defs>
|
||
<linearGradient id="grad" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
|
||
<stop stop-color="#6366f1"/>
|
||
<stop offset="1" stop-color="#8b5cf6"/>
|
||
</linearGradient>
|
||
</defs>
|
||
</svg>
|
||
<span class="brand-name">VvvebJS <em>Manager</em></span>
|
||
</div>
|
||
<div class="topbar-actions">
|
||
<span class="site-count" id="site-count">— 個專案</span>
|
||
<button class="btn btn-primary" id="new-project-btn">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||
</svg>
|
||
新增專案
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- ── 主內容 ─────────────────────────────────────────────────── -->
|
||
<main class="main-content">
|
||
|
||
<!-- 搜尋列 -->
|
||
<div class="search-bar-wrap">
|
||
<div class="search-bar">
|
||
<svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||
</svg>
|
||
<input type="text" id="search-input" placeholder="搜尋專案名稱…" autocomplete="off">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 卡片網格 -->
|
||
<div class="projects-grid" id="projects-grid">
|
||
<!-- 由 JS 動態填入 -->
|
||
</div>
|
||
|
||
<!-- 空狀態 -->
|
||
<div class="empty-state" id="empty-state" style="display:none">
|
||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2">
|
||
<rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/>
|
||
<line x1="12" y1="17" x2="12" y2="21"/>
|
||
</svg>
|
||
<h2>尚無專案</h2>
|
||
<p>點擊右上角「新增專案」開始建立您的第一個網站</p>
|
||
<button class="btn btn-primary" id="empty-new-btn">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||
</svg>
|
||
建立第一個專案
|
||
</button>
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<!-- ── 新增專案 Modal ──────────────────────────────────────────── -->
|
||
<div class="modal-backdrop" id="create-modal-backdrop">
|
||
<div class="modal-box" id="create-modal" role="dialog" aria-labelledby="create-modal-title" aria-modal="true">
|
||
<div class="modal-header">
|
||
<h2 id="create-modal-title">新增專案</h2>
|
||
<button class="modal-close" id="create-modal-close" aria-label="關閉">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label for="new-project-name">專案名稱 <span class="required">*</span></label>
|
||
<input type="text" id="new-project-name" placeholder="例如:我的公司網站" autocomplete="off">
|
||
<span class="slug-preview" id="slug-preview"></span>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="new-project-desc">描述(選填)</label>
|
||
<textarea id="new-project-desc" rows="3" placeholder="簡短說明這個網站的用途…"></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-ghost" id="create-cancel-btn">取消</button>
|
||
<button class="btn btn-primary" id="create-confirm-btn">建立專案</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── 設定 Offcanvas ─────────────────────────────────────────── -->
|
||
<div class="offcanvas-backdrop" id="settings-backdrop">
|
||
<div class="offcanvas" id="settings-panel" role="dialog" aria-labelledby="settings-title">
|
||
<div class="offcanvas-header">
|
||
<h2 id="settings-title">專案設定</h2>
|
||
<button class="modal-close" id="settings-close" aria-label="關閉">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="offcanvas-body">
|
||
<input type="hidden" id="settings-slug">
|
||
<div class="form-group">
|
||
<label for="settings-name">專案名稱</label>
|
||
<input type="text" id="settings-name" placeholder="專案名稱">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="settings-desc">描述</label>
|
||
<textarea id="settings-desc" rows="4"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Slug(資料夾名稱)</label>
|
||
<input type="text" id="settings-slug-display" disabled class="input-disabled">
|
||
<p class="hint-text">Slug 為唯一識別碼,建立後無法更改</p>
|
||
</div>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<h3 class="section-label">頁面清單</h3>
|
||
<ul class="pages-list" id="settings-pages-list">
|
||
<li class="pages-loading">載入中…</li>
|
||
</ul>
|
||
<button class="btn btn-ghost btn-sm" id="add-page-btn" style="margin-top:0.75rem; width:100%">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||
</svg>
|
||
新增頁面
|
||
</button>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<div class="danger-zone">
|
||
<h3 class="section-label danger">危險操作</h3>
|
||
<button class="btn btn-danger" id="delete-project-btn">
|
||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/>
|
||
<path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/>
|
||
</svg>
|
||
刪除此專案
|
||
</button>
|
||
<p class="hint-text danger">此操作不可復原,將刪除所有頁面檔案</p>
|
||
</div>
|
||
</div>
|
||
<div class="offcanvas-footer">
|
||
<button class="btn btn-ghost" id="settings-cancel-btn">取消</button>
|
||
<button class="btn btn-primary" id="settings-save-btn">儲存設定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── 刪除確認 Modal ──────────────────────────────────────────── -->
|
||
<div class="modal-backdrop" id="delete-modal-backdrop">
|
||
<div class="modal-box modal-sm" role="dialog" aria-modal="true">
|
||
<div class="modal-header">
|
||
<h2>確認刪除</h2>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>確定要刪除專案 <strong id="delete-project-name"></strong> 嗎?<br>
|
||
此操作不可復原,所有頁面檔案都將被刪除。</p>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-ghost" id="delete-cancel-btn">取消</button>
|
||
<button class="btn btn-danger" id="delete-confirm-btn">確認刪除</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Toast 通知 ─────────────────────────────────────────────── -->
|
||
<div class="toast-container" id="toast-container"></div>
|
||
|
||
<script>
|
||
// ── 狀態 ─────────────────────────────────────────────────────────
|
||
let allProjects = [];
|
||
let settingsCurrentSlug = '';
|
||
let deleteTargetSlug = '';
|
||
|
||
// ── API 輔助 ─────────────────────────────────────────────────────
|
||
async function apiFetch(url, opts = {}) {
|
||
const res = await fetch(url, {
|
||
headers: { 'Content-Type': 'application/json' },
|
||
...opts,
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
||
return data;
|
||
}
|
||
|
||
// ── Toast ─────────────────────────────────────────────────────────
|
||
function showToast(msg, type = 'success') {
|
||
const tc = document.getElementById('toast-container');
|
||
const t = document.createElement('div');
|
||
t.className = `toast toast-${type}`;
|
||
t.textContent = msg;
|
||
tc.appendChild(t);
|
||
requestAnimationFrame(() => t.classList.add('show'));
|
||
setTimeout(() => {
|
||
t.classList.remove('show');
|
||
setTimeout(() => t.remove(), 400);
|
||
}, 3000);
|
||
}
|
||
|
||
// ── Slug 預覽 ─────────────────────────────────────────────────────
|
||
function toSlug(s) {
|
||
return s.toLowerCase()
|
||
.replace(/[^\w\s-]/g, '')
|
||
.replace(/[\s_-]+/g, '-')
|
||
.replace(/^-+|-+$/g, '') || 'untitled';
|
||
}
|
||
|
||
// ── 渲染卡片 ─────────────────────────────────────────────────────
|
||
function renderProjects(projects) {
|
||
const grid = document.getElementById('projects-grid');
|
||
const empty = document.getElementById('empty-state');
|
||
const count = document.getElementById('site-count');
|
||
count.textContent = `${projects.length} 個專案`;
|
||
|
||
if (projects.length === 0) {
|
||
grid.innerHTML = '';
|
||
empty.style.display = 'flex';
|
||
return;
|
||
}
|
||
empty.style.display = 'none';
|
||
|
||
grid.innerHTML = projects.map(p => {
|
||
const mod = p.last_modified ? p.last_modified.slice(0, 10) : '—';
|
||
const pages = p.page_count ?? 0;
|
||
return `
|
||
<div class="project-card" data-slug="${p.slug}">
|
||
<div class="card-glow"></div>
|
||
<div class="card-header-row">
|
||
<div class="card-icon">
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||
<rect x="2" y="3" width="20" height="14" rx="2"/>
|
||
<line x1="8" y1="21" x2="16" y2="21"/>
|
||
<line x1="12" y1="17" x2="12" y2="21"/>
|
||
</svg>
|
||
</div>
|
||
<div class="card-menu">
|
||
<button class="icon-btn settings-btn" data-slug="${p.slug}" title="設定">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="12" cy="12" r="3"/>
|
||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<h3 class="card-title">${escHtml(p.name)}</h3>
|
||
<p class="card-desc">${escHtml(p.description || '—')}</p>
|
||
</div>
|
||
<div class="card-meta">
|
||
<span class="meta-item">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||
<polyline points="14 2 14 8 20 8"/>
|
||
</svg>
|
||
${pages} 頁
|
||
</span>
|
||
<span class="meta-item">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||
<line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/>
|
||
<line x1="3" y1="10" x2="21" y2="10"/>
|
||
</svg>
|
||
${mod}
|
||
</span>
|
||
</div>
|
||
<div class="card-actions">
|
||
<a href="/editor/${p.slug}" class="btn btn-primary btn-sm" id="edit-btn-${p.slug}">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||
</svg>
|
||
開啟編輯器
|
||
</a>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
// 綁定設定按鈕
|
||
grid.querySelectorAll('.settings-btn').forEach(btn => {
|
||
btn.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
openSettings(btn.dataset.slug);
|
||
});
|
||
});
|
||
}
|
||
|
||
function escHtml(s) {
|
||
return String(s)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
// ── 載入專案 ─────────────────────────────────────────────────────
|
||
async function loadProjects() {
|
||
try {
|
||
allProjects = await apiFetch('/api/projects');
|
||
renderProjects(allProjects);
|
||
} catch (e) {
|
||
showToast('載入專案失敗:' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
// ── 搜尋 ─────────────────────────────────────────────────────────
|
||
document.getElementById('search-input').addEventListener('input', function() {
|
||
const q = this.value.toLowerCase();
|
||
const filtered = allProjects.filter(p =>
|
||
p.name.toLowerCase().includes(q) ||
|
||
(p.description || '').toLowerCase().includes(q)
|
||
);
|
||
renderProjects(filtered);
|
||
});
|
||
|
||
// ── 新增專案 Modal ────────────────────────────────────────────────
|
||
function openCreateModal() {
|
||
document.getElementById('new-project-name').value = '';
|
||
document.getElementById('new-project-desc').value = '';
|
||
document.getElementById('slug-preview').textContent = '';
|
||
document.getElementById('create-modal-backdrop').classList.add('active');
|
||
setTimeout(() => document.getElementById('new-project-name').focus(), 50);
|
||
}
|
||
function closeCreateModal() {
|
||
document.getElementById('create-modal-backdrop').classList.remove('active');
|
||
}
|
||
|
||
document.getElementById('new-project-btn').addEventListener('click', openCreateModal);
|
||
document.getElementById('empty-new-btn').addEventListener('click', openCreateModal);
|
||
document.getElementById('create-modal-close').addEventListener('click', closeCreateModal);
|
||
document.getElementById('create-cancel-btn').addEventListener('click', closeCreateModal);
|
||
document.getElementById('create-modal-backdrop').addEventListener('click', e => {
|
||
if (e.target.id === 'create-modal-backdrop') closeCreateModal();
|
||
});
|
||
|
||
document.getElementById('new-project-name').addEventListener('input', function() {
|
||
const slug = toSlug(this.value);
|
||
const preview = document.getElementById('slug-preview');
|
||
preview.textContent = this.value ? `資料夾名稱:${slug}` : '';
|
||
});
|
||
|
||
document.getElementById('create-confirm-btn').addEventListener('click', async () => {
|
||
const name = document.getElementById('new-project-name').value.trim();
|
||
const desc = document.getElementById('new-project-desc').value.trim();
|
||
if (!name) {
|
||
showToast('請輸入專案名稱', 'error');
|
||
document.getElementById('new-project-name').focus();
|
||
return;
|
||
}
|
||
try {
|
||
const btn = document.getElementById('create-confirm-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = '建立中…';
|
||
await apiFetch('/api/projects', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ name, description: desc }),
|
||
});
|
||
closeCreateModal();
|
||
showToast(`專案「${name}」建立成功!`);
|
||
await loadProjects();
|
||
} catch(e) {
|
||
showToast('建立失敗:' + e.message, 'error');
|
||
} finally {
|
||
const btn = document.getElementById('create-confirm-btn');
|
||
btn.disabled = false;
|
||
btn.textContent = '建立專案';
|
||
}
|
||
});
|
||
|
||
// ── 設定 Offcanvas ────────────────────────────────────────────────
|
||
async function openSettings(slug) {
|
||
settingsCurrentSlug = slug;
|
||
document.getElementById('settings-backdrop').classList.add('active');
|
||
document.getElementById('settings-slug').value = slug;
|
||
document.getElementById('settings-slug-display').value = slug;
|
||
document.getElementById('settings-pages-list').innerHTML = '<li class="pages-loading">載入中…</li>';
|
||
|
||
try {
|
||
const proj = await apiFetch(`/api/projects/${slug}`);
|
||
document.getElementById('settings-name').value = proj.name;
|
||
document.getElementById('settings-desc').value = proj.description || '';
|
||
|
||
const pages = proj.pages || [];
|
||
const listEl = document.getElementById('settings-pages-list');
|
||
if (pages.length === 0) {
|
||
listEl.innerHTML = '<li class="no-pages">尚無頁面</li>';
|
||
} else {
|
||
listEl.innerHTML = pages.map(p => `
|
||
<li class="page-item" style="justify-content: space-between; display: flex; align-items: center; margin-bottom: 0.4rem;">
|
||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: var(--accent);">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||
<polyline points="14 2 14 8 20 8"/>
|
||
</svg>
|
||
<span>${escHtml(p)}</span>
|
||
</div>
|
||
<a href="/editor/${slug}?page=${escHtml(p)}" class="btn btn-primary btn-sm" style="padding: 0.2rem 0.6rem; font-size: 0.75rem; gap: 0.25rem; border-radius: var(--radius-sm);">
|
||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<path d="M11 4H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||
</svg>
|
||
編輯
|
||
</a>
|
||
</li>`).join('');
|
||
}
|
||
} catch(e) {
|
||
showToast('載入設定失敗:' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
function closeSettings() {
|
||
document.getElementById('settings-backdrop').classList.remove('active');
|
||
}
|
||
|
||
document.getElementById('settings-close').addEventListener('click', closeSettings);
|
||
document.getElementById('settings-cancel-btn').addEventListener('click', closeSettings);
|
||
document.getElementById('settings-backdrop').addEventListener('click', e => {
|
||
if (e.target.id === 'settings-backdrop') closeSettings();
|
||
});
|
||
|
||
document.getElementById('settings-save-btn').addEventListener('click', async () => {
|
||
const name = document.getElementById('settings-name').value.trim();
|
||
const desc = document.getElementById('settings-desc').value.trim();
|
||
if (!name) {
|
||
showToast('名稱不能為空', 'error');
|
||
return;
|
||
}
|
||
try {
|
||
await apiFetch(`/api/projects/${settingsCurrentSlug}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ name, description: desc }),
|
||
});
|
||
closeSettings();
|
||
showToast('設定已儲存');
|
||
await loadProjects();
|
||
} catch(e) {
|
||
showToast('儲存失敗:' + e.message, 'error');
|
||
}
|
||
});
|
||
|
||
// 新增頁面
|
||
document.getElementById('add-page-btn').addEventListener('click', async () => {
|
||
const name = prompt('請輸入新頁面名稱(例如:about):');
|
||
if (!name) return;
|
||
try {
|
||
await apiFetch(`/api/projects/${settingsCurrentSlug}/pages`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ name }),
|
||
});
|
||
showToast(`頁面「${name}」建立成功`);
|
||
openSettings(settingsCurrentSlug); // 重新整理面板
|
||
} catch(e) {
|
||
showToast('建立頁面失敗:' + e.message, 'error');
|
||
}
|
||
});
|
||
|
||
// ── 刪除 ─────────────────────────────────────────────────────────
|
||
document.getElementById('delete-project-btn').addEventListener('click', () => {
|
||
deleteTargetSlug = settingsCurrentSlug;
|
||
const proj = allProjects.find(p => p.slug === settingsCurrentSlug);
|
||
document.getElementById('delete-project-name').textContent = proj ? proj.name : deleteTargetSlug;
|
||
document.getElementById('delete-modal-backdrop').classList.add('active');
|
||
});
|
||
|
||
document.getElementById('delete-cancel-btn').addEventListener('click', () => {
|
||
document.getElementById('delete-modal-backdrop').classList.remove('active');
|
||
});
|
||
|
||
document.getElementById('delete-confirm-btn').addEventListener('click', async () => {
|
||
try {
|
||
await apiFetch(`/api/projects/${deleteTargetSlug}`, { method: 'DELETE' });
|
||
document.getElementById('delete-modal-backdrop').classList.remove('active');
|
||
closeSettings();
|
||
showToast('專案已刪除');
|
||
await loadProjects();
|
||
} catch(e) {
|
||
showToast('刪除失敗:' + e.message, 'error');
|
||
}
|
||
});
|
||
|
||
document.getElementById('delete-modal-backdrop').addEventListener('click', e => {
|
||
if (e.target.id === 'delete-modal-backdrop')
|
||
document.getElementById('delete-modal-backdrop').classList.remove('active');
|
||
});
|
||
|
||
// ── Enter 鍵快速提交 ─────────────────────────────────────────────
|
||
document.getElementById('new-project-name').addEventListener('keydown', e => {
|
||
if (e.key === 'Enter') document.getElementById('create-confirm-btn').click();
|
||
});
|
||
|
||
// ── 初始化 ────────────────────────────────────────────────────────
|
||
loadProjects();
|
||
</script>
|
||
</body>
|
||
</html>
|