Files
vvveb-cms/templates/project_dashboard.html

204 lines
10 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>專案管理</title>
<link rel="stylesheet" href="/static/css/my-style.css">
</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">專案:<span id="projName"></span></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="pagesTreeContainer">
<div id="pagesTree" style="padding:8px;border:1px solid #eee;background:#fff"></div>
<div style="margin-top:8px"><button id="saveTreeBtn">儲存頁面結構</button></div>
</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';
}));
// Extract slug from path /dashboard/project/<slug>
const parts = location.pathname.split('/');
const slug = parts.length >= 4 ? decodeURIComponent(parts[3]) : null;
if(!slug){ document.getElementById('overview').innerText='未提供專案'; }
else document.getElementById('projName').innerText = slug;
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('儲存失敗');
});
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; 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('上傳失敗'); } });
if(slug) loadSettings(), listMedia();
// pages tree: improved rendering with expand/collapse and drag/drop reorder
async function loadPagesTree(){
const res = await fetch(`/api/projects/${encodeURIComponent(slug)}/pages-tree`);
if(!res.ok) return; const tree = await res.json();
const container = document.getElementById('pagesTree'); container.innerHTML='';
function createLi(item){
const li = document.createElement('li');
li.className = 'tree-item';
li.style.padding='6px';
li.draggable = true;
li.dataset.type = item.type || 'file';
li.dataset.name = item.name || item.title || '';
const titleWrap = document.createElement('span'); titleWrap.className='tree-title';
titleWrap.textContent = item.title || item.name || '';
const btns = document.createElement('span'); btns.style.float = 'right';
btns.innerHTML = "<button class='btn-edit' title='編輯'>✏️</button> <button class='btn-toggle' title='展開/收合'></button>";
li.appendChild(titleWrap); li.appendChild(btns);
// children container
if(item.children && item.children.length){
const childUl = renderList(item.children);
childUl.style.marginLeft = '18px';
childUl.style.paddingLeft = '8px';
li.appendChild(childUl);
}
// drag handlers
li.addEventListener('dragstart', (e)=>{ e.dataTransfer.setData('text/name', encodeURIComponent(li.dataset.name)); e.currentTarget.style.opacity=0.4; });
li.addEventListener('dragend', (e)=>{ e.currentTarget.style.opacity=1; });
// allow dropping onto li (insert before/after) and into folder (append)
li.addEventListener('dragover', (e)=>{ e.preventDefault(); e.dataTransfer.dropEffect='move'; li.style.background='#f7f7f7'; });
li.addEventListener('dragleave', (e)=>{ li.style.background=''; });
li.addEventListener('drop', (e)=>{
e.preventDefault(); li.style.background='';
const nameEnc = e.dataTransfer.getData('text/name'); if(!nameEnc) return;
const name = decodeURIComponent(nameEnc);
const src = container.querySelector(`[data-name='${CSS.escape(name)}']`);
if(!src || src === li) return;
// if dropping onto an item that has children (folder-like), append as child
const onTop = e.clientY < (li.getBoundingClientRect().top + li.getBoundingClientRect().height/2);
if(li.querySelector('ul')){
// append as last child
li.querySelector('ul').appendChild(src);
} else if(onTop){
li.parentNode.insertBefore(src, li);
} else {
if(li.nextSibling) li.parentNode.insertBefore(src, li.nextSibling); else li.parentNode.appendChild(src);
}
});
// toggle button handler
const toggleBtn = btns.querySelector('.btn-toggle');
if(toggleBtn){
toggleBtn.addEventListener('click', ()=>{
const child = li.querySelector('ul');
if(child){ child.style.display = (child.style.display === 'none') ? '' : 'none'; toggleBtn.textContent = child.style.display === 'none' ? '▶' : '▼'; }
});
}
return li;
}
function renderList(items){
const ul = document.createElement('ul'); ul.style.listStyle='none'; ul.style.paddingLeft='4px'; ul.style.margin='0';
items.forEach(it=> ul.appendChild(createLi(it)) );
return ul;
}
if(tree && tree.root) container.appendChild(renderList(tree.root));
}
// build payload from DOM
function buildPayload(rootEl){
function walk(ul){
const arr = [];
const lis = Array.from(ul.children).filter(n=>n.tagName === 'LI');
lis.forEach(li=>{
const title = li.querySelector('.tree-title')?.textContent || '';
const name = li.dataset.name || '';
const type = li.dataset.type || 'file';
const obj = { name: name, title: title, type: type };
const childUl = li.querySelector(':scope > ul');
if(childUl) obj.children = walk(childUl);
arr.push(obj);
});
return arr;
}
return { root: walk(rootEl.querySelector(':scope > ul') || document.createElement('ul')) };
}
document.getElementById('saveTreeBtn').addEventListener('click', async ()=>{
const container = document.getElementById('pagesTree');
const payload = buildPayload(container);
const btn = document.getElementById('saveTreeBtn'); btn.disabled = true; btn.textContent = '儲存中...';
try{
const res = await fetch(`/api/projects/${encodeURIComponent(slug)}/pages-tree`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)});
if(res.ok) alert('頁面結構已儲存'); else { alert('儲存失敗'); }
}catch(e){ alert('儲存失敗'); }
btn.disabled = false; btn.textContent = '儲存頁面結構';
});
if(slug){ loadPagesTree(); }
</script>
</body>
</html>