Files
tangfamily/public/admin.html
2026-06-24 22:13:01 +08:00

358 lines
16 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>唐氏族谱 - 后台管理</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Microsoft YaHei', Arial, sans-serif; background: #f0f7ee; min-height: 100vh; }
/* 顶栏 */
.topbar {
background: #fff; border-bottom: 2px solid #c5e1a5;
padding: 10px 16px; display: flex; align-items: center;
justify-content: space-between; position: sticky; top: 0; z-index: 200;
}
.topbar h1 { font-size: 18px; color: #558b2f; }
.topbar-right { display: flex; gap: 6px; align-items: center; }
.tbtn {
padding: 5px 12px; border-radius: 6px; border: 1.5px solid #c5e1a5;
background: #f1f8e9; color: #558b2f; font-size: 13px; cursor: pointer; transition: background .15s;
}
.tbtn:hover { background: #dcedc8; }
.tbtn.green { background: #8bc34a; color: #fff; border-color: #8bc34a; }
.tbtn.green:hover { background: #7cb342; }
.tbtn.red { background: #ef9a9a; color: #fff; border-color: #ef9a9a; }
.tbtn.red:hover { background: #e57373; }
.tbtn.active { background: #8bc34a; color: #fff; border-color: #8bc34a; }
.tbtn.active:hover { background: #7cb342; }
/* 主体 */
.main { padding: 14px 16px; max-width: 960px; margin: 0 auto; }
/* 快速新增行 */
.quick-add {
background: #fff; border: 1.5px solid #c5e1a5; border-radius: 10px;
padding: 10px 14px; margin-bottom: 12px;
display: grid;
grid-template-columns: 1fr 80px 70px 1.4fr 1fr 1fr auto;
gap: 8px; align-items: center;
}
.quick-add input, .quick-add select {
padding: 6px 9px; border: 1.5px solid #d4e8c2; border-radius: 6px;
font-size: 13px; background: #f8fdf3; width: 100%;
}
.quick-add input:focus, .quick-add select:focus { outline: none; border-color: #8bc34a; }
.quick-add label { display: none; }
/* 成员表格 */
.table-wrap { background: #fff; border: 1.5px solid #c5e1a5; border-radius: 10px; overflow: hidden; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th {
background: #f1f8e9; color: #558b2f; padding: 8px 10px;
text-align: left; font-weight: 600; border-bottom: 1.5px solid #c5e1a5;
white-space: nowrap;
}
tbody tr { border-bottom: 1px solid #f0f7ee; transition: background .15s; }
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: #f9fef5; }
td { padding: 6px 10px; color: #333; vertical-align: middle; }
td.name-cell { font-weight: 600; color: #2e4a1a; }
td.gender-m { color: #1976d2; }
td.gender-f { color: #c2185b; }
/* 行内编辑 */
.row-edit input, .row-edit select {
padding: 4px 6px; border: 1.5px solid #a5d6a7; border-radius: 5px;
font-size: 13px; background: #f8fdf3; width: 100%;
}
.row-edit input:focus, .row-edit select:focus { outline: none; border-color: #66bb6a; }
.actions { display: flex; gap: 5px; white-space: nowrap; }
/* 空/加载 */
.tip { text-align: center; padding: 40px 0; color: #bbb; font-size: 14px; }
/* 提示 toast */
#toast {
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
background: #558b2f; color: #fff; padding: 8px 22px;
border-radius: 20px; font-size: 13px; opacity: 0;
transition: opacity .3s; pointer-events: none; z-index: 9999;
}
#toast.show { opacity: 1; }
/* 响应式:小屏隐藏部分列 */
@media (max-width: 700px) {
.quick-add { grid-template-columns: 1fr 70px 60px auto; }
.quick-add .col-father, .quick-add .col-mother, .quick-add .col-gen { display: none; }
.hide-sm { display: none; }
}
@media (max-width: 480px) {
.quick-add { grid-template-columns: 1fr 60px auto; }
.quick-add .col-gender { display: none; }
}
</style>
</head>
<body>
<div class="topbar">
<h1>族谱管理后台</h1>
<div class="topbar-right">
<span id="countBadge" style="color:#888;font-size:12px;"></span>
<button class="tbtn" id="yearToggle" onclick="toggleYear()">显示年份</button>
<button class="tbtn" onclick="location.href='/tree.html'">查看族谱</button>
</div>
</div>
<div class="main">
<!-- 快速新增 -->
<form id="quickForm" class="quick-add" onsubmit="quickAdd(event)">
<input id="qName" placeholder="姓名 *" required>
<input id="qYear" type="number" placeholder="出生年" min="1900" max="2100" required>
<select id="qGender" class="col-gender" required>
<option value="">性别</option>
<option value="男"></option>
<option value="女"></option>
</select>
<select id="qGen" class="col-gen">
<option value="">辈分(选填)</option>
<option value="鼻祖">鼻祖</option>
<option value="远祖">远祖</option>
<option value="太祖">太祖</option>
<option value="烈祖">烈祖</option>
<option value="天祖">天祖</option>
<option value="高祖">高祖</option>
<option value="曾祖">曾祖</option>
<option value="祖父">祖父</option>
<option value="父亲">父亲</option>
<option value="自己">自己</option>
<option value="子女">子女</option>
<option value="孙辈">孙辈</option>
<option value="曾孙">曾孙</option>
<option value="玄孙">玄孙</option>
<option value="来孙">来孙</option>
<option value="晜孙">晜孙</option>
<option value="仍孙">仍孙</option>
<option value="云孙">云孙</option>
<option value="耳孙">耳孙</option>
</select>
<select id="qFather" class="col-father"><option value="">父亲(选填)</option></select>
<select id="qMother" class="col-mother"><option value="">母亲(选填)</option></select>
<button type="submit" class="tbtn green">+ 添加</button>
</form>
<!-- 成员列表 -->
<div class="table-wrap">
<table>
<thead>
<tr>
<th>姓名</th>
<th>出生年</th>
<th>性别</th>
<th class="hide-sm">辈分</th>
<th class="hide-sm">父亲</th>
<th class="hide-sm">母亲</th>
<th>操作</th>
</tr>
</thead>
<tbody id="tbody">
<tr><td colspan="7" class="tip">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
<div id="toast"></div>
<script>
const token = localStorage.getItem('token');
const isAdmin = localStorage.getItem('isAdmin') === 'true';
if (!token || !isAdmin) location.href = '/';
let members = [];
let editingId = null;
// ── 显示年份开关 ──────────────────────────────────────
function getShowYear() { return localStorage.getItem('showYear') !== 'false'; }
function toggleYear() {
const next = !getShowYear();
localStorage.setItem('showYear', next ? 'true' : 'false');
updateYearToggleBtn();
}
function updateYearToggleBtn() {
const btn = document.getElementById('yearToggle');
if (!btn) return;
const on = getShowYear();
btn.textContent = on ? '显示年份:开' : '显示年份:关';
btn.classList.toggle('active', on);
}
updateYearToggleBtn();
const GEN_OPTIONS = [
'鼻祖','远祖','太祖','烈祖','天祖',
'高祖','曾祖','祖父','父亲','自己',
'子女','孙辈','曾孙','玄孙','来孙',
'晜孙','仍孙','云孙','耳孙'
];
function genSelectOpts(current) {
return `<option value="">无</option>` +
GEN_OPTIONS.map(g => `<option value="${g}" ${current === g ? 'selected' : ''}>${g}</option>`).join('');
}
// ── 数据加载 ──────────────────────────────────────────
async function loadMembers() {
try {
const res = await fetch('/api/members', { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error();
members = await res.json();
members.sort((a, b) => a.birthYear - b.birthYear);
renderTable();
updateSelects();
document.getElementById('countBadge').textContent = `${members.length}`;
} catch {
document.getElementById('tbody').innerHTML = '<tr><td colspan="7" class="tip" style="color:#e57373">加载失败,请刷新</td></tr>';
}
}
// ── 渲染表格 ──────────────────────────────────────────
function renderTable() {
const map = {};
members.forEach(m => map[m.id] = m);
const tbody = document.getElementById('tbody');
if (members.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="tip">暂无成员,请在上方快速添加</td></tr>';
return;
}
tbody.innerHTML = members.map(m => {
if (editingId === m.id) return renderEditRow(m, map);
const father = m.fatherId && map[m.fatherId] ? map[m.fatherId].name : '—';
const mother = m.motherId && map[m.motherId] ? map[m.motherId].name : '—';
return `<tr id="row-${m.id}">
<td class="name-cell">${m.name}</td>
<td>${m.birthYear}</td>
<td class="${m.gender === '男' ? 'gender-m' : 'gender-f'}">${m.gender}</td>
<td class="hide-sm">${m.generation || '—'}</td>
<td class="hide-sm">${father}</td>
<td class="hide-sm">${mother}</td>
<td><div class="actions">
<button class="tbtn" onclick="startEdit('${m.id}')">编辑</button>
<button class="tbtn red" onclick="deleteMember('${m.id}','${m.name}')">删除</button>
</div></td>
</tr>`;
}).join('');
}
function renderEditRow(m, map) {
const males = members.filter(x => x.gender === '男' && x.id !== m.id);
const females = members.filter(x => x.gender === '女' && x.id !== m.id);
const fOpts = `<option value="">无</option>` + males.map(x => `<option value="${x.id}" ${m.fatherId === x.id ? 'selected' : ''}>${x.name}(${x.birthYear})</option>`).join('');
const mOpts = `<option value="">无</option>` + females.map(x => `<option value="${x.id}" ${m.motherId === x.id ? 'selected' : ''}>${x.name}(${x.birthYear})</option>`).join('');
return `<tr id="row-${m.id}" class="row-edit" style="background:#f1f8e9">
<td><input id="eName" value="${m.name}" required></td>
<td><input id="eYear" type="number" value="${m.birthYear}" min="1900" max="2100" style="width:70px"></td>
<td><select id="eGender">
<option value="男" ${m.gender==='男'?'selected':''}>男</option>
<option value="女" ${m.gender==='女'?'selected':''}>女</option>
</select></td>
<td class="hide-sm"><select id="eGen">${genSelectOpts(m.generation || '')}</select></td>
<td class="hide-sm"><select id="eFather">${fOpts}</select></td>
<td class="hide-sm"><select id="eMother">${mOpts}</select></td>
<td><div class="actions">
<button class="tbtn green" onclick="saveEdit('${m.id}')">保存</button>
<button class="tbtn" onclick="cancelEdit()">取消</button>
</div></td>
</tr>`;
}
// ── 编辑相关 ──────────────────────────────────────────
function startEdit(id) {
editingId = id;
renderTable();
const row = document.getElementById(`row-${id}`);
if (row) row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function cancelEdit() {
editingId = null;
renderTable();
}
async function saveEdit(id) {
const name = document.getElementById('eName').value.trim();
const birthYear = parseInt(document.getElementById('eYear').value);
const gender = document.getElementById('eGender').value;
const generation = document.getElementById('eGen') ? document.getElementById('eGen').value || undefined : undefined;
const fatherId = document.getElementById('eFather') ? document.getElementById('eFather').value || undefined : undefined;
const motherId = document.getElementById('eMother') ? document.getElementById('eMother').value || undefined : undefined;
if (!name || !birthYear || !gender) { toast('请填写完整信息', true); return; }
await saveMember({ id, name, birthYear, gender, generation, fatherId, motherId });
editingId = null;
}
// ── 快速添加 ──────────────────────────────────────────
async function quickAdd(e) {
e.preventDefault();
const name = document.getElementById('qName').value.trim();
const birthYear = parseInt(document.getElementById('qYear').value);
const gender = document.getElementById('qGender').value;
const generation = document.getElementById('qGen').value || undefined;
const fatherId = document.getElementById('qFather').value || undefined;
const motherId = document.getElementById('qMother').value || undefined;
if (!name || !birthYear || !gender) { toast('请填写姓名、出生年、性别', true); return; }
await saveMember({ name, birthYear, gender, generation, fatherId, motherId });
document.getElementById('quickForm').reset();
}
// ── 公共保存 ──────────────────────────────────────────
async function saveMember(member) {
try {
const res = await fetch('/api/members', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(member)
});
if (!res.ok) throw new Error();
toast(member.id ? '已保存' : '已添加');
await loadMembers();
} catch { toast('保存失败,请重试', true); }
}
// ── 删除 ──────────────────────────────────────────────
async function deleteMember(id, name) {
if (!confirm(`确定删除「${name}」?`)) return;
try {
const res = await fetch('/api/members/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ id })
});
if (!res.ok) throw new Error();
toast('已删除');
await loadMembers();
} catch { toast('删除失败', true); }
}
// ── 更新父母下拉选项 ──────────────────────────────────
function updateSelects() {
const males = members.filter(m => m.gender === '男');
const females = members.filter(m => m.gender === '女');
document.getElementById('qFather').innerHTML = '<option value="">父亲(选填)</option>' +
males.map(m => `<option value="${m.id}">${m.name}(${m.birthYear})</option>`).join('');
document.getElementById('qMother').innerHTML = '<option value="">母亲(选填)</option>' +
females.map(m => `<option value="${m.id}">${m.name}(${m.birthYear})</option>`).join('');
}
// ── Toast 提示 ────────────────────────────────────────
function toast(msg, err = false) {
const el = document.getElementById('toast');
el.textContent = msg;
el.style.background = err ? '#e57373' : '#558b2f';
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 2000);
}
loadMembers();
</script>
</body>
</html>