358 lines
16 KiB
HTML
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>
|