|
|
@@ -77,7 +77,10 @@
|
|
|
}
|
|
|
.filter-controls { display: flex; align-items: center; gap: 5px; font-size: 0.8rem; }
|
|
|
.filter-controls input[type=range] { width: 80px; }
|
|
|
- .page-nav { margin-bottom: 10px; display: flex; gap: 10px; align-items: center; }
|
|
|
+ .page-nav { margin-bottom: 10px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
|
|
+ #imageTabNav .nav-link { cursor: pointer; font-size: 0.9rem; padding: 0.35rem 0.75rem; }
|
|
|
+ .reference-empty-state { color: #6c757d; padding: 40px 20px; text-align: center; }
|
|
|
+ .reference-empty-state i { font-size: 2.5rem; display: block; margin-bottom: 10px; }
|
|
|
.section-title { border-left: 4px solid #0d6efd; padding-left: 10px; margin: 25px 0 15px; font-weight: bold; color: #333; }
|
|
|
|
|
|
.father-lineage-hint {
|
|
|
@@ -107,8 +110,11 @@
|
|
|
</div>
|
|
|
<div class="card-body">
|
|
|
<form method="POST">
|
|
|
- <input type="hidden" name="source_record_id" value="{{ source_record_id if source_record_id else (member.source_record_id if member and member.source_record_id else '') }}">
|
|
|
+ <input type="hidden" name="source_record_id" value="{{ source_record_id or '' }}">
|
|
|
<input type="hidden" name="source_index" value="">
|
|
|
+ <input type="hidden" name="reference_oss_url" id="referenceOssUrl" value="{{ member.reference_oss_url if member and member.reference_oss_url else '' }}">
|
|
|
+ <input type="hidden" name="reference_file_name" id="referenceFileName" value="{{ member.reference_file_name if member and member.reference_file_name else '' }}">
|
|
|
+ <input type="hidden" name="delete_reference" id="deleteReference" value="0">
|
|
|
<div class="section-title">核心信息 (必填)</div>
|
|
|
<div class="row g-3 mb-4">
|
|
|
<div class="col-md-6">
|
|
|
@@ -462,22 +468,53 @@
|
|
|
|
|
|
<!-- 右侧:图片参考 -->
|
|
|
<div class="image-panel">
|
|
|
- <div class="page-nav">
|
|
|
- <label class="fw-bold">扫描件参考:</label>
|
|
|
- <button id="aiBtn" onclick="recognizeImage()" class="btn btn-sm btn-info text-white ms-2 me-2">
|
|
|
- <i class="bi bi-magic"></i> AI 识别
|
|
|
- </button>
|
|
|
- <input type="number" id="pageInput" class="form-control form-control-sm" style="width: 70px;" placeholder="页码">
|
|
|
- <button onclick="gotoPage()" class="btn btn-sm btn-primary">跳转</button>
|
|
|
- <div class="ms-auto small text-muted">
|
|
|
- 当前: <span id="currentPage">1</span> / <span id="totalPages">{{ images|length }}</span>
|
|
|
+ <ul class="nav nav-tabs mb-2" id="imageTabNav">
|
|
|
+ <li class="nav-item">
|
|
|
+ <button type="button" class="nav-link active" id="tab-scan" onclick="switchImageTab('scan')">
|
|
|
+ <i class="bi bi-file-earmark-image"></i> 查看扫描件
|
|
|
+ </button>
|
|
|
+ </li>
|
|
|
+ <li class="nav-item">
|
|
|
+ <button type="button" class="nav-link" id="tab-reference" onclick="switchImageTab('reference')">
|
|
|
+ <i class="bi bi-file-earmark-plus"></i> 查看参考件
|
|
|
+ </button>
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ <div id="scanTabPanel">
|
|
|
+ <div class="page-nav">
|
|
|
+ <label class="fw-bold">扫描件参考:</label>
|
|
|
+ <button id="aiBtn" onclick="recognizeImage()" class="btn btn-sm btn-info text-white ms-2 me-2">
|
|
|
+ <i class="bi bi-magic"></i> AI 识别
|
|
|
+ </button>
|
|
|
+ <input type="number" id="pageInput" class="form-control form-control-sm" style="width: 70px;" placeholder="页码">
|
|
|
+ <button type="button" onclick="gotoPage()" class="btn btn-sm btn-primary">跳转</button>
|
|
|
+ <div class="ms-auto small text-muted">
|
|
|
+ 当前: <span id="currentPage">1</span> / <span id="totalPages">{{ images|length }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="mb-2 small text-muted" id="imageMetadata" style="display: none;">
|
|
|
+ <span class="me-2"><i class="bi bi-journal-text"></i> 版本名称: <span id="metaVersion">-</span></span>
|
|
|
+ <span class="me-2"><i class="bi bi-archive"></i> 版本来源: <span id="metaSource">-</span></span>
|
|
|
+ <span><i class="bi bi-person"></i> 提供人: <span id="metaPerson">-</span></span>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="mb-2 small text-muted" id="imageMetadata" style="display: none;">
|
|
|
- <span class="me-2"><i class="bi bi-journal-text"></i> 版本名称: <span id="metaVersion">-</span></span>
|
|
|
- <span class="me-2"><i class="bi bi-archive"></i> 版本来源: <span id="metaSource">-</span></span>
|
|
|
- <span><i class="bi bi-person"></i> 提供人: <span id="metaPerson">-</span></span>
|
|
|
+
|
|
|
+ <div id="referenceTabPanel" style="display: none;">
|
|
|
+ <div class="page-nav">
|
|
|
+ <label class="fw-bold">参考件:</label>
|
|
|
+ <input type="file" id="referenceFileInput" accept="image/jpeg,image/png,image/gif,image/webp" style="display: none;">
|
|
|
+ <button type="button" onclick="document.getElementById('referenceFileInput').click()" class="btn btn-sm btn-primary ms-2">
|
|
|
+ <i class="bi bi-cloud-upload"></i> 上传参考件
|
|
|
+ </button>
|
|
|
+ <button type="button" id="deleteReferenceBtn" onclick="deleteReference()" class="btn btn-sm btn-outline-danger ms-2" style="display: none;">
|
|
|
+ <i class="bi bi-trash"></i> 删除参考件
|
|
|
+ </button>
|
|
|
+ <span class="small text-muted ms-2">支持 Ctrl+V 粘贴图片</span>
|
|
|
+ <span class="ms-auto small text-muted" id="referenceFileLabel"></span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+
|
|
|
<div class="image-toolbar rounded-top">
|
|
|
<div class="btn-group btn-group-sm">
|
|
|
<button type="button" class="btn btn-outline-secondary" onclick="rotateImage(-90)" title="左旋90°"><i class="bi bi-arrow-counterclockwise"></i></button>
|
|
|
@@ -498,19 +535,21 @@
|
|
|
<div class="image-viewer shadow-inner" id="viewer">
|
|
|
<div id="magnifier" class="magnifier-glass"></div>
|
|
|
<div id="imageWrapper" class="image-wrapper">
|
|
|
- {% if images %}
|
|
|
- <img id="refImage" src="{{ images[0].oss_url }}" alt="家谱图片" draggable="false">
|
|
|
- {% else %}
|
|
|
- <div class="mt-5 text-muted">
|
|
|
- <i class="bi bi-image fs-1 d-block mb-2"></i>
|
|
|
- 暂无上传的家谱图片
|
|
|
- </div>
|
|
|
- {% endif %}
|
|
|
+ <img id="refImage" src="" alt="家谱图片" draggable="false" style="display: none;">
|
|
|
+ <div id="scanEmptyState" class="reference-empty-state" style="display: none;">
|
|
|
+ <i class="bi bi-image"></i>
|
|
|
+ 暂无关联扫描件
|
|
|
+ </div>
|
|
|
+ <div id="referenceEmptyState" class="reference-empty-state" style="display: none;">
|
|
|
+ <i class="bi bi-file-earmark-plus"></i>
|
|
|
+ 暂无参考件,请上传或粘贴散页图片<br>
|
|
|
+ <span class="small">点击「上传参考件」或按 Ctrl+V / ⌘+V 粘贴</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="mt-2 d-flex justify-content-between">
|
|
|
- <button onclick="prevImage()" class="btn btn-sm btn-outline-secondary">上一张</button>
|
|
|
- <button onclick="nextImage()" class="btn btn-sm btn-outline-secondary">下一张</button>
|
|
|
+ <div id="scanNavButtons" class="mt-2 d-flex justify-content-between">
|
|
|
+ <button type="button" onclick="prevImage()" class="btn btn-sm btn-outline-secondary">上一张</button>
|
|
|
+ <button type="button" onclick="nextImage()" class="btn btn-sm btn-outline-secondary">下一张</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -662,6 +701,12 @@
|
|
|
{% endfor %}
|
|
|
];
|
|
|
let currentIndex = 0;
|
|
|
+ let activeImageTab = 'scan';
|
|
|
+ let referenceImageUrl = "{{ member.reference_image_url if member and member.reference_image_url else '' }}";
|
|
|
+ let referenceOssUrlRaw = "{{ member.reference_oss_url if member and member.reference_oss_url else '' }}";
|
|
|
+ const memberIdForReference = {{ member.id if member else 'null' }};
|
|
|
+ const hasScanSource = {{ 'true' if source_record_id else 'false' }};
|
|
|
+ const hasReferenceInitial = {{ 'true' if (member and member.reference_oss_url) else 'false' }};
|
|
|
|
|
|
// 初始化时根据source_record_id设置正确的扫描件
|
|
|
{% if source_record_id %}
|
|
|
@@ -698,9 +743,164 @@
|
|
|
|
|
|
// 页面加载完成后更新显示
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
- // 调用updateDisplay函数显示正确的扫描件
|
|
|
- updateDisplay();
|
|
|
+ activeImageTab = hasScanSource ? 'scan' : (hasReferenceInitial ? 'reference' : 'scan');
|
|
|
+ switchImageTab(activeImageTab, true);
|
|
|
+ document.getElementById('referenceFileInput').addEventListener('change', handleReferenceFileSelect);
|
|
|
+ document.addEventListener('paste', handleReferencePaste);
|
|
|
+ updateReferenceControls();
|
|
|
});
|
|
|
+
|
|
|
+ function isReferencePasteTarget(element) {
|
|
|
+ if (!element) return false;
|
|
|
+ const tag = element.tagName;
|
|
|
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
|
+ if (element.isContentEditable) return true;
|
|
|
+ return !!element.closest('.form-panel');
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleReferencePaste(event) {
|
|
|
+ if (activeImageTab !== 'reference') return;
|
|
|
+ if (isReferencePasteTarget(event.target)) return;
|
|
|
+
|
|
|
+ const items = event.clipboardData && event.clipboardData.items;
|
|
|
+ if (!items) return;
|
|
|
+
|
|
|
+ for (const item of items) {
|
|
|
+ if (!item.type.startsWith('image/')) continue;
|
|
|
+ event.preventDefault();
|
|
|
+ const blob = item.getAsFile();
|
|
|
+ if (!blob) return;
|
|
|
+ const ext = item.type === 'image/jpeg' ? 'jpg' : (item.type.split('/')[1] || 'png');
|
|
|
+ const file = new File([blob], `paste_${Date.now()}.${ext}`, { type: item.type });
|
|
|
+ uploadReferenceFile(file);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function switchImageTab(tab, skipPersist) {
|
|
|
+ activeImageTab = tab;
|
|
|
+ document.getElementById('tab-scan').classList.toggle('active', tab === 'scan');
|
|
|
+ document.getElementById('tab-reference').classList.toggle('active', tab === 'reference');
|
|
|
+ document.getElementById('scanTabPanel').style.display = tab === 'scan' ? 'block' : 'none';
|
|
|
+ document.getElementById('referenceTabPanel').style.display = tab === 'reference' ? 'block' : 'none';
|
|
|
+ document.getElementById('scanNavButtons').style.display = tab === 'scan' ? 'flex' : 'none';
|
|
|
+ if (tab === 'scan') {
|
|
|
+ updateDisplay();
|
|
|
+ } else {
|
|
|
+ updateReferenceDisplay();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateReferenceControls() {
|
|
|
+ const deleteBtn = document.getElementById('deleteReferenceBtn');
|
|
|
+ const fileLabel = document.getElementById('referenceFileLabel');
|
|
|
+ const fileName = document.getElementById('referenceFileName').value;
|
|
|
+ const hasRef = !!referenceImageUrl;
|
|
|
+ deleteBtn.style.display = hasRef ? 'inline-block' : 'none';
|
|
|
+ fileLabel.textContent = fileName ? `当前: ${fileName}` : '';
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateReferenceDisplay() {
|
|
|
+ const img = document.getElementById('refImage');
|
|
|
+ const scanEmpty = document.getElementById('scanEmptyState');
|
|
|
+ const refEmpty = document.getElementById('referenceEmptyState');
|
|
|
+ const metaContainer = document.getElementById('imageMetadata');
|
|
|
+ if (metaContainer) metaContainer.style.display = 'none';
|
|
|
+
|
|
|
+ resetFilters();
|
|
|
+ if (referenceImageUrl) {
|
|
|
+ img.src = referenceImageUrl;
|
|
|
+ img.style.display = 'block';
|
|
|
+ refEmpty.style.display = 'none';
|
|
|
+ scanEmpty.style.display = 'none';
|
|
|
+ } else {
|
|
|
+ img.style.display = 'none';
|
|
|
+ img.removeAttribute('src');
|
|
|
+ refEmpty.style.display = 'block';
|
|
|
+ scanEmpty.style.display = 'none';
|
|
|
+ }
|
|
|
+ currentX = 0;
|
|
|
+ currentY = 0;
|
|
|
+ isZoomedIn = false;
|
|
|
+ updateImageTransform();
|
|
|
+ }
|
|
|
+
|
|
|
+ async function handleReferenceFileSelect(event) {
|
|
|
+ const file = event.target.files && event.target.files[0];
|
|
|
+ event.target.value = '';
|
|
|
+ if (file) await uploadReferenceFile(file);
|
|
|
+ }
|
|
|
+
|
|
|
+ async function uploadReferenceFile(file) {
|
|
|
+ if (!file) return;
|
|
|
+ if (!file.type.startsWith('image/')) {
|
|
|
+ alert('请选择图片文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (file.size > 10 * 1024 * 1024) {
|
|
|
+ alert('图片大小不能超过 10MB');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('file', file);
|
|
|
+ const uploadBtn = document.querySelector('#referenceTabPanel .btn-primary');
|
|
|
+ const originalHtml = uploadBtn.innerHTML;
|
|
|
+ uploadBtn.disabled = true;
|
|
|
+ uploadBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 上传中...';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const url = memberIdForReference
|
|
|
+ ? `/manager/api/member/${memberIdForReference}/reference`
|
|
|
+ : '/manager/api/upload_reference';
|
|
|
+ const resp = await fetch(url, { method: 'POST', body: formData });
|
|
|
+ const result = await resp.json();
|
|
|
+ if (!resp.ok || !result.success) {
|
|
|
+ throw new Error(result.message || '上传失败');
|
|
|
+ }
|
|
|
+ referenceImageUrl = result.oss_url;
|
|
|
+ referenceOssUrlRaw = result.oss_url_raw || result.oss_url;
|
|
|
+ document.getElementById('referenceOssUrl').value = referenceOssUrlRaw;
|
|
|
+ document.getElementById('referenceFileName').value = result.file_name || file.name;
|
|
|
+ document.getElementById('deleteReference').value = '0';
|
|
|
+ updateReferenceControls();
|
|
|
+ if (activeImageTab === 'reference') {
|
|
|
+ updateReferenceDisplay();
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ alert(error.message || '上传失败,请稍后重试');
|
|
|
+ } finally {
|
|
|
+ uploadBtn.disabled = false;
|
|
|
+ uploadBtn.innerHTML = originalHtml;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function deleteReference() {
|
|
|
+ if (!confirm('确定要删除参考件吗?')) return;
|
|
|
+
|
|
|
+ if (memberIdForReference) {
|
|
|
+ try {
|
|
|
+ const resp = await fetch(`/manager/api/member/${memberIdForReference}/reference`, { method: 'DELETE' });
|
|
|
+ const result = await resp.json();
|
|
|
+ if (!resp.ok || !result.success) {
|
|
|
+ throw new Error(result.message || '删除失败');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ alert(error.message || '删除失败,请稍后重试');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ referenceImageUrl = '';
|
|
|
+ referenceOssUrlRaw = '';
|
|
|
+ document.getElementById('referenceOssUrl').value = '';
|
|
|
+ document.getElementById('referenceFileName').value = '';
|
|
|
+ document.getElementById('deleteReference').value = '1';
|
|
|
+ updateReferenceControls();
|
|
|
+ if (activeImageTab === 'reference') {
|
|
|
+ updateReferenceDisplay();
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
// Initialize Dragging and Zooming
|
|
|
if (imageWrapper) {
|
|
|
@@ -1290,18 +1490,45 @@
|
|
|
// --- End AJAX Form Submission ---
|
|
|
|
|
|
function updateDisplay() {
|
|
|
+ if (activeImageTab !== 'scan') return;
|
|
|
+ const img = document.getElementById('refImage');
|
|
|
+ const scanEmpty = document.getElementById('scanEmptyState');
|
|
|
+ const refEmpty = document.getElementById('referenceEmptyState');
|
|
|
+ const isEditMode = {{ 'true' if member else 'false' }};
|
|
|
+ const metaContainer = document.getElementById('imageMetadata');
|
|
|
+ const aiBtn = document.getElementById('aiBtn');
|
|
|
+
|
|
|
+ if (isEditMode && !hasScanSource) {
|
|
|
+ img.style.display = 'none';
|
|
|
+ img.removeAttribute('src');
|
|
|
+ scanEmpty.innerHTML = '<i class="bi bi-image"></i> 暂无关联扫描件';
|
|
|
+ scanEmpty.style.display = 'block';
|
|
|
+ refEmpty.style.display = 'none';
|
|
|
+ if (metaContainer) metaContainer.style.display = 'none';
|
|
|
+ if (aiBtn) {
|
|
|
+ aiBtn.innerHTML = '<i class="bi bi-magic"></i> AI 识别';
|
|
|
+ aiBtn.className = 'btn btn-sm btn-info text-white ms-2 me-2';
|
|
|
+ aiBtn.onclick = recognizeImage;
|
|
|
+ }
|
|
|
+ resetFilters();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
if (images.length > 0) {
|
|
|
- const img = images[currentIndex];
|
|
|
- document.getElementById('refImage').src = img.url;
|
|
|
+ const imageData = images[currentIndex];
|
|
|
+ img.src = imageData.url;
|
|
|
+ img.style.display = 'block';
|
|
|
+ scanEmpty.style.display = 'none';
|
|
|
+ refEmpty.style.display = 'none';
|
|
|
document.getElementById('currentPage').innerText = currentIndex + 1;
|
|
|
|
|
|
// Update metadata display
|
|
|
const metaContainer = document.getElementById('imageMetadata');
|
|
|
- if (img.genealogy_version || img.genealogy_source || img.upload_person) {
|
|
|
+ if (imageData.genealogy_version || imageData.genealogy_source || imageData.upload_person) {
|
|
|
metaContainer.style.display = 'block';
|
|
|
- document.getElementById('metaVersion').innerText = img.genealogy_version || '未提供';
|
|
|
- document.getElementById('metaSource').innerText = img.genealogy_source || '未提供';
|
|
|
- document.getElementById('metaPerson').innerText = img.upload_person || '未提供';
|
|
|
+ document.getElementById('metaVersion').innerText = imageData.genealogy_version || '未提供';
|
|
|
+ document.getElementById('metaSource').innerText = imageData.genealogy_source || '未提供';
|
|
|
+ document.getElementById('metaPerson').innerText = imageData.upload_person || '未提供';
|
|
|
} else {
|
|
|
metaContainer.style.display = 'none';
|
|
|
}
|
|
|
@@ -1313,7 +1540,7 @@
|
|
|
// Otherwise, use the current image's ID
|
|
|
const isEditMode = {{ 'true' if member else 'false' }};
|
|
|
if (!isEditMode) {
|
|
|
- sourceRecordIdField.value = img.id;
|
|
|
+ sourceRecordIdField.value = imageData.id;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1333,9 +1560,9 @@
|
|
|
if (resultCount) resultCount.innerText = '0';
|
|
|
if (resultList) resultList.innerHTML = '';
|
|
|
|
|
|
- if (img.ai_status === 2 && img.ai_content) {
|
|
|
+ if (imageData.ai_status === 2 && imageData.ai_content) {
|
|
|
// Determine content
|
|
|
- let content = img.ai_content;
|
|
|
+ let content = imageData.ai_content;
|
|
|
// Parse if string (it might be a string if double encoded or stored as JSON string in DB)
|
|
|
if (typeof content === 'string') {
|
|
|
try { content = JSON.parse(content); } catch(e) { content = []; }
|
|
|
@@ -1356,12 +1583,18 @@
|
|
|
};
|
|
|
return; // Done
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
+ }
|
|
|
+
|
|
|
// Default: Reset to "AI Recognition"
|
|
|
aiBtn.innerHTML = '<i class="bi bi-magic"></i> AI 识别';
|
|
|
aiBtn.className = 'btn btn-sm btn-info text-white ms-2 me-2';
|
|
|
aiBtn.onclick = recognizeImage;
|
|
|
+ } else {
|
|
|
+ img.style.display = 'none';
|
|
|
+ img.removeAttribute('src');
|
|
|
+ scanEmpty.innerHTML = '<i class="bi bi-image"></i> 暂无上传的家谱图片';
|
|
|
+ scanEmpty.style.display = 'block';
|
|
|
+ refEmpty.style.display = 'none';
|
|
|
}
|
|
|
}
|
|
|
|