| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391 |
- {% extends "layout.html" %}
- {% block title %}代数连续性核查 - 家谱管理系统{% endblock %}
- {% block extra_css %}
- <style>
- /* ── 统计 Tab 卡片 ── */
- .stat-tab {
- border-radius: 10px;
- padding: 16px 24px;
- cursor: pointer;
- border: 2px solid transparent;
- transition: border-color .18s, box-shadow .18s, background .18s;
- user-select: none;
- min-width: 160px;
- }
- .stat-tab:hover { box-shadow: 0 2px 12px rgba(0,0,0,.10); }
- .stat-tab.active-tab {
- border-color: currentColor;
- box-shadow: 0 3px 14px rgba(0,0,0,.13);
- }
- .stat-tab .stat-num { font-size: 2.1rem; font-weight: 700; line-height: 1; }
- .stat-tab .stat-label{ font-size: 13px; color: #6c757d; margin-top: 4px; }
- .stat-tab.tab-error .stat-num { color: #dc3545; }
- .stat-tab.tab-warn .stat-num { color: #f59e0b; }
- .stat-tab.tab-prefix .stat-num { color: #198754; }
- .stat-tab.tab-error.active-tab { border-color: #dc3545; background:#fff5f5; }
- .stat-tab.tab-warn.active-tab { border-color: #f59e0b; background:#fffbeb; }
- /* ── 分组标题 ── */
- .prefix-group-header {
- background: #f0f4fb; font-weight: 600; color: #2c4a8a;
- padding: 8px 16px; border-left: 4px solid #4a69bd;
- border-radius: 4px; margin: 18px 0 6px;
- display: flex; align-items: center; justify-content: space-between;
- }
- .warn-group-header {
- background: #fffbeb; font-weight: 600; color: #92400e;
- padding: 8px 16px; border-left: 4px solid #f59e0b;
- border-radius: 4px; margin: 18px 0 6px;
- }
- /* ── 徽章 ── */
- .gen-badge {
- display: inline-block; padding: 1px 8px;
- border-radius: 12px; font-size: 12px; font-weight: 600;
- }
- .gen-badge.correct { background:#d1fae5; color:#065f46; }
- .gen-badge.wrong { background:#fee2e2; color:#991b1b; }
- .gen-badge.expected { background:#dbeafe; color:#1e40af; }
- .gen-badge.missing { background:#fef3c7; color:#92400e; }
- .diff-badge {
- display: inline-block; padding: 1px 7px;
- border-radius: 10px; font-size: 11px; font-weight: 700;
- }
- .diff-badge.big { background:#fef3c7; color:#92400e; }
- .diff-badge.skip { background:#ffe4e6; color:#be123c; }
- .member-link { color:#3b5998; text-decoration:none; font-weight:600; }
- .member-link:hover { text-decoration:underline; }
- .error-row { font-size: 13px; }
- /* ── 筛选栏 ── */
- .filter-bar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:16px; }
- .filter-bar select, .filter-bar input {
- padding:5px 10px; border:1px solid #d1d5db;
- border-radius:6px; font-size:13px;
- }
- /* ── 加载遮罩 ── */
- #loadingOverlay {
- position:fixed; inset:0; background:rgba(255,255,255,.85);
- display:flex; flex-direction:column; align-items:center;
- justify-content:center; z-index:9999; font-size:15px; color:#555; gap:14px;
- }
- .spinner {
- width:40px; height:40px; border:4px solid #e5e7eb;
- border-top-color:#4a69bd; border-radius:50%;
- animation:spin .8s linear infinite;
- }
- @keyframes spin { to { transform:rotate(360deg); } }
- </style>
- {% endblock %}
- {% block content %}
- <div id="loadingOverlay">
- <div class="spinner"></div>
- <div>正在核查所有父子代数关系,请稍候…</div>
- </div>
- <div class="d-flex justify-content-between align-items-center mb-4">
- <h2><i class="bi bi-clipboard2-check"></i> 代数连续性核查</h2>
- <div class="d-flex gap-2">
- <button class="btn btn-outline-primary btn-sm" onclick="reloadCheck()">
- <i class="bi bi-arrow-clockwise"></i> 重新核查
- </button>
- <a href="{{ url_for('members') }}" class="btn btn-outline-secondary btn-sm">返回成员列表</a>
- </div>
- </div>
- <!-- 统计 Tab 卡片 -->
- <div class="row g-3 mb-4" id="statsRow" style="display:none!important;">
- <div class="col-auto">
- <div class="stat-tab tab-error bg-white shadow-sm active-tab" id="tabError" onclick="switchTab('error')">
- <div class="stat-num" id="statErrors">-</div>
- <div class="stat-label">代数不连续(需修正)</div>
- </div>
- </div>
- <div class="col-auto">
- <div class="stat-tab tab-warn bg-white shadow-sm" id="tabWarn" onclick="switchTab('warn')">
- <div class="stat-num" id="statWarnings">-</div>
- <div class="stat-label">代数缺失(需补录)</div>
- </div>
- </div>
- <div class="col-auto">
- <div class="stat-tab tab-prefix bg-white shadow-sm" style="cursor:default;">
- <div class="stat-num" id="statPrefixes">-</div>
- <div class="stat-label">涉及支系</div>
- </div>
- </div>
- </div>
- <!-- 不连续:筛选栏 -->
- <div class="filter-bar" id="filterBarError" style="display:none!important;">
- <label style="font-size:13px;font-weight:600;">支系筛选:</label>
- <select id="prefixFilter" onchange="renderErrors()"><option value="">全部支系</option></select>
- <label style="font-size:13px;font-weight:600;">关键字:</label>
- <input type="text" id="keywordFilter" placeholder="姓名/ID" oninput="renderErrors()" style="width:140px;">
- <label style="font-size:13px;font-weight:600;">差值:</label>
- <select id="diffFilter" onchange="renderErrors()">
- <option value="">全部</option>
- <option value="0">跳代(差值≠1)</option>
- <option value="neg">倒退(子<父+1)</option>
- <option value="big">跨越(差值>1)</option>
- </select>
- <span id="filteredCount" style="font-size:12px;color:#6b7280;margin-left:4px;"></span>
- </div>
- <!-- 代数缺失:筛选栏 -->
- <div class="filter-bar" id="filterBarWarn" style="display:none!important;">
- <label style="font-size:13px;font-weight:600;">关键字:</label>
- <input type="text" id="keywordFilterWarn" placeholder="姓名/ID" oninput="renderWarnings()" style="width:160px;">
- <label style="font-size:13px;font-weight:600;">缺失方:</label>
- <select id="missingSideFilter" onchange="renderWarnings()">
- <option value="">全部</option>
- <option value="child">子女缺失</option>
- <option value="parent">父亲缺失</option>
- </select>
- <span id="filteredWarnCount" style="font-size:12px;color:#6b7280;margin-left:4px;"></span>
- </div>
- <!-- 不连续列表 -->
- <div id="errorsContainer"></div>
- <!-- 代数缺失列表 -->
- <div id="warningsContainer" style="display:none;"></div>
- <script>
- let _allErrors = [];
- let _allWarnings = [];
- let _activeTab = 'error';
- /* ── Tab 切换 ── */
- function switchTab(tab) {
- _activeTab = tab;
- const isError = (tab === 'error');
- document.getElementById('tabError').classList.toggle('active-tab', isError);
- document.getElementById('tabWarn').classList.toggle('active-tab', !isError);
- document.getElementById('filterBarError').style.cssText = isError ? '' : 'display:none!important;';
- document.getElementById('filterBarWarn').style.cssText = isError ? 'display:none!important;' : '';
- document.getElementById('errorsContainer').style.display = isError ? '' : 'none';
- document.getElementById('warningsContainer').style.display = isError ? 'none' : '';
- }
- /* ── 加载数据 ── */
- async function loadCheck() {
- try {
- const res = await fetch('/manager/api/generation_check');
- const data = await res.json();
- document.getElementById('loadingOverlay').style.display = 'none';
- if (!data.success) {
- document.getElementById('errorsContainer').innerHTML =
- `<div class="alert alert-danger">核查失败:${data.message}</div>`;
- return;
- }
- _allErrors = data.errors || [];
- _allWarnings = data.warnings || [];
- const prefixes = [...new Set(_allErrors.map(e => e.prefix))];
- document.getElementById('statErrors').textContent = data.error_count;
- document.getElementById('statWarnings').textContent = data.warning_count;
- document.getElementById('statPrefixes').textContent = prefixes.length;
- document.getElementById('statsRow').style.cssText = '';
- // 填充支系选项
- const sel = document.getElementById('prefixFilter');
- prefixes.sort().forEach(p => {
- const opt = document.createElement('option');
- opt.value = p; opt.textContent = p || '(无前缀)';
- sel.appendChild(opt);
- });
- switchTab('error'); // 默认展示不连续
- renderErrors();
- renderWarnings();
- } catch (e) {
- document.getElementById('loadingOverlay').style.display = 'none';
- document.getElementById('errorsContainer').innerHTML =
- `<div class="alert alert-danger">请求失败:${e.message}</div>`;
- }
- }
- /* ── 渲染:代数不连续 ── */
- function renderErrors() {
- const prefixF = document.getElementById('prefixFilter').value;
- const keyword = document.getElementById('keywordFilter').value.trim().toLowerCase();
- const diffF = document.getElementById('diffFilter').value;
- let filtered = _allErrors.filter(e => {
- if (prefixF && e.prefix !== prefixF) return false;
- if (keyword) {
- const hay = `${e.parent_name}${e.child_name}${e.parent_id}${e.child_id}`.toLowerCase();
- if (!hay.includes(keyword)) return false;
- }
- if (diffF === '0' && e.diff === 1) return false;
- if (diffF === 'neg' && e.diff >= 1) return false;
- if (diffF === 'big' && e.diff <= 1) return false;
- return true;
- });
- document.getElementById('filteredCount').textContent =
- `显示 ${filtered.length} / ${_allErrors.length} 条`;
- if (filtered.length === 0) {
- document.getElementById('errorsContainer').innerHTML =
- _allErrors.length === 0
- ? '<div class="alert alert-success mt-2"><i class="bi bi-check-circle"></i> 所有父子代数均连续,无需修正!</div>'
- : '<div class="alert alert-info mt-2">当前筛选条件无匹配结果。</div>';
- return;
- }
- const groups = {};
- filtered.forEach(e => {
- const key = e.prefix || '(无前缀)';
- if (!groups[key]) groups[key] = [];
- groups[key].push(e);
- });
- let html = '';
- for (const [prefix, items] of Object.entries(groups)) {
- html += `<div class="prefix-group-header">
- <span>📌 ${prefix} — ${items.length} 处不连续</span>
- </div>
- <div class="card shadow-sm border-0 mb-2"><table class="table table-hover table-sm mb-0">
- <thead class="table-light"><tr>
- <th style="width:36px;">#</th>
- <th>父亲</th><th style="width:80px;">父代数</th>
- <th>子女</th><th style="width:80px;">子代数</th>
- <th style="width:80px;">应为</th><th style="width:80px;">差值</th>
- <th style="width:70px;">操作</th>
- </tr></thead><tbody>`;
- items.forEach((e, i) => {
- const diffLabel = e.diff === 0 ? '相同'
- : e.diff < 0 ? `倒退${Math.abs(e.diff)}代`
- : `跨越${e.diff - 1}代`;
- const diffCls = e.diff <= 0 ? 'skip' : e.diff > 1 ? 'big' : '';
- html += `<tr class="error-row">
- <td class="text-muted">${i+1}</td>
- <td>
- <a href="/manager/member_detail/${e.parent_id}" target="_blank" class="member-link">${e.parent_name}</a>
- <span class="text-muted" style="font-size:11px;"> ID:${e.parent_id}</span>
- <div style="font-size:10px;color:#aaa;">${e.parent_gen_raw}</div>
- </td>
- <td><span class="gen-badge correct">第${e.parent_gen}代</span></td>
- <td>
- <a href="/manager/member_detail/${e.child_id}" target="_blank" class="member-link">${e.child_name}</a>
- <span class="text-muted" style="font-size:11px;"> ID:${e.child_id}</span>
- <div style="font-size:10px;color:#aaa;">${e.child_gen_raw}</div>
- </td>
- <td><span class="gen-badge wrong">第${e.child_gen}代</span></td>
- <td><span class="gen-badge expected">第${e.expected_child_gen}代</span></td>
- <td>${diffCls
- ? `<span class="diff-badge ${diffCls}">${diffLabel}</span>`
- : `<span class="text-muted" style="font-size:11px;">${diffLabel}</span>`}</td>
- <td>
- <a href="/manager/member_detail/${e.child_id}" target="_blank"
- class="btn btn-outline-primary btn-sm py-0 px-2" style="font-size:11px;">修正子</a>
- </td>
- </tr>`;
- });
- html += `</tbody></table></div>`;
- }
- document.getElementById('errorsContainer').innerHTML = html;
- }
- /* ── 渲染:代数缺失 ── */
- function renderWarnings() {
- const keyword = (document.getElementById('keywordFilterWarn').value || '').trim().toLowerCase();
- const missingSide = document.getElementById('missingSideFilter').value;
- let filtered = _allWarnings.filter(w => {
- if (keyword) {
- const hay = `${w.parent_name}${w.child_name}${w.parent_id}${w.child_id}`.toLowerCase();
- if (!hay.includes(keyword)) return false;
- }
- const childMissing = !w.child_gen_raw;
- const parentMissing = !w.parent_gen_raw;
- if (missingSide === 'child' && !childMissing) return false;
- if (missingSide === 'parent' && !parentMissing) return false;
- return true;
- });
- document.getElementById('filteredWarnCount').textContent =
- `显示 ${filtered.length} / ${_allWarnings.length} 条`;
- if (filtered.length === 0) {
- document.getElementById('warningsContainer').innerHTML =
- _allWarnings.length === 0
- ? '<div class="alert alert-success mt-2"><i class="bi bi-check-circle"></i> 所有父子均已填写代数!</div>'
- : '<div class="alert alert-info mt-2">当前筛选条件无匹配结果。</div>';
- return;
- }
- let html = `<div class="warn-group-header">
- <i class="bi bi-exclamation-triangle me-2"></i>
- 共 ${filtered.length} 对父子关系中至少一方未填写可解析的代数字段
- </div>
- <div class="card shadow-sm border-0 mb-2"><table class="table table-hover table-sm mb-0">
- <thead class="table-light"><tr>
- <th style="width:36px;">#</th>
- <th>父亲</th><th style="width:120px;">父代数字段</th>
- <th>子女</th><th style="width:120px;">子代数字段</th>
- <th style="width:100px;">操作</th>
- </tr></thead><tbody>`;
- filtered.forEach((w, i) => {
- const pGen = w.parent_gen_raw
- ? `<code style="font-size:11px;">${w.parent_gen_raw}</code>`
- : '<span class="gen-badge missing">未填</span>';
- const cGen = w.child_gen_raw
- ? `<code style="font-size:11px;">${w.child_gen_raw}</code>`
- : '<span class="gen-badge missing">未填</span>';
- html += `<tr class="error-row">
- <td class="text-muted">${i+1}</td>
- <td>
- <a href="/manager/member_detail/${w.parent_id}" target="_blank" class="member-link">${w.parent_name}</a>
- <span class="text-muted" style="font-size:11px;"> ID:${w.parent_id}</span>
- </td>
- <td>${pGen}</td>
- <td>
- <a href="/manager/member_detail/${w.child_id}" target="_blank" class="member-link">${w.child_name}</a>
- <span class="text-muted" style="font-size:11px;"> ID:${w.child_id}</span>
- </td>
- <td>${cGen}</td>
- <td>
- <a href="/manager/member_detail/${w.parent_id}" target="_blank"
- class="btn btn-outline-secondary btn-sm py-0 px-2 me-1" style="font-size:11px;">查父</a>
- <a href="/manager/member_detail/${w.child_id}" target="_blank"
- class="btn btn-outline-warning btn-sm py-0 px-2" style="font-size:11px;">补录子</a>
- </td>
- </tr>`;
- });
- html += `</tbody></table></div>`;
- document.getElementById('warningsContainer').innerHTML = html;
- }
- /* ── 重新核查 ── */
- function reloadCheck() {
- _activeTab = 'error';
- document.getElementById('loadingOverlay').style.display = 'flex';
- document.getElementById('errorsContainer').innerHTML = '';
- document.getElementById('warningsContainer').innerHTML = '';
- document.getElementById('statsRow').style.cssText = 'display:none!important;';
- document.getElementById('filterBarError').style.cssText = 'display:none!important;';
- document.getElementById('filterBarWarn').style.cssText = 'display:none!important;';
- // 重置支系下拉
- const sel = document.getElementById('prefixFilter');
- while (sel.options.length > 1) sel.remove(1);
- loadCheck();
- }
- loadCheck();
- </script>
- {% endblock %}
|