| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569 |
- {% extends "layout.html" %}
- {% block title %}上传管理 - 家谱管理系统{% endblock %}
- {% block extra_css %}
- <style>
- .thumbnail-img {
- width: 50px;
- height: 70px;
- object-fit: cover;
- border-radius: 4px;
- cursor: pointer;
- border: 1px solid #dee2e6;
- transition: transform 0.2s;
- background-color: #f8f9fa;
- }
- .thumbnail-img:hover {
- transform: scale(1.5);
- box-shadow: 0 4px 8px rgba(0,0,0,0.15);
- z-index: 10;
- position: relative;
- }
- /* Modal Image Viewer Styles */
- .modal-image-container {
- height: 85vh;
- display: flex;
- flex-direction: column;
- }
- .image-toolbar {
- background: #e9ecef;
- padding: 5px 10px;
- border-bottom: 1px solid #dee2e6;
- display: flex;
- gap: 10px;
- align-items: center;
- flex-wrap: wrap;
- }
- .image-viewer {
- flex: 1;
- border: 1px solid #ccc;
- background: #f0f0f0;
- overflow: hidden;
- text-align: center;
- position: relative;
- cursor: grab;
- user-select: none;
- }
- .image-viewer:active {
- cursor: grabbing;
- }
- .image-wrapper {
- display: inline-block;
- transition: transform 0.2s ease-out;
- transform-origin: center center;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- }
- .image-wrapper img {
- max-width: 100%;
- max-height: 100%;
- display: block;
- pointer-events: none;
- user-select: none;
- transition: filter 0.2s;
- box-shadow: 0 0 20px rgba(0,0,0,0.1);
- }
- .magnifier-glass {
- position: absolute;
- border: 3px solid #000;
- border-radius: 50%;
- cursor: none;
- width: 150px;
- height: 150px;
- box-shadow: 0 0 10px rgba(0,0,0,0.5);
- display: none;
- z-index: 1000;
- background-repeat: no-repeat;
- background-color: white;
- pointer-events: none;
- }
- </style>
- {% endblock %}
- {% block content %}
- <div class="d-flex justify-content-between align-items-center mb-4">
- <h2><i class="bi bi-file-earmark-arrow-up"></i> 家谱扫描件管理</h2>
- <a href="{{ url_for('upload') }}" class="btn btn-primary">
- <i class="bi bi-cloud-upload me-1"></i> 上传家谱文件
- </a>
- </div>
- <div class="card shadow-sm mb-4">
- <div class="card-body">
- <form method="GET" action="{{ url_for('index') }}" class="row g-3 align-items-end">
- <div class="col-md-2">
- <label class="form-label">版本名称</label>
- <input type="text" name="version" class="form-control" value="{{ version }}" placeholder="模糊搜索">
- </div>
- <div class="col-md-2">
- <label class="form-label">版本来源</label>
- <input type="text" name="source" class="form-control" value="{{ source }}" placeholder="模糊搜索">
- </div>
- <div class="col-md-2">
- <label class="form-label">提供人</label>
- <input type="text" name="person" class="form-control" value="{{ person }}" placeholder="模糊搜索">
- </div>
- <div class="col-md-2">
- <label class="form-label">文件类型</label>
- <select name="file_type" class="form-select">
- <option value="">全部</option>
- <option value="图片" {% if file_type == '图片' %}selected{% endif %}>图片</option>
- <option value="PDF" {% if file_type == 'PDF' %}selected{% endif %}>PDF</option>
- </select>
- </div>
- <div class="col-md-4">
- <button type="submit" class="btn btn-primary"><i class="bi bi-search"></i> 搜索</button>
- <a href="{{ url_for('index') }}" class="btn btn-outline-secondary">重置</a>
- </div>
- </form>
- </div>
- </div>
- <div class="card shadow-sm">
- <div class="card-body p-0">
- <div class="table-responsive">
- <table class="table table-hover mb-0">
- <thead class="table-light">
- <tr>
- <th class="px-4">文件名</th>
- <th>页码</th>
- <th>提供人</th>
- <th>AI 解析状态</th>
- <th>上传时间</th>
- <th class="text-center">操作</th>
- </tr>
- </thead>
- <tbody>
- {% for record in records %}
- <tr>
- <td class="px-4">
- <div class="d-flex align-items-center">
- <img src="{{ record.oss_url }}" class="thumbnail-img me-3 list-preview-img" data-url="{{ record.oss_url }}" data-page="{{ record.page_number or '' }}" title="点击预览" onclick="openImageViewer('{{ record.oss_url }}', '{{ record.page_number or '' }}')">
- <div class="text-break fw-bold">
- {% if record.file_type == 'PDF' %}
- <span class="badge bg-danger me-1">PDF</span>
- {% elif record.file_type == '图片' %}
- <span class="badge bg-success me-1">图片</span>
- {% endif %}
- {{ record.file_name }}
- </div>
- </div>
- </td>
- <td>
- {% if record.page_number %}
- <span class="badge bg-info text-dark">第 {{ record.page_number }} 页</span>
- {% else %}
- <span class="text-muted">未知</span>
- {% endif %}
- </td>
- <td>
- <div class="fw-bold">{{ record.upload_person or '未知' }}</div>
- {% if record.genealogy_version or record.genealogy_source %}
- <div class="small text-muted mt-1" style="font-size: 0.75rem;">
- {% if record.genealogy_version %}
- <span title="版本名称"><i class="bi bi-tag"></i> {{ record.genealogy_version }}</span><br>
- {% endif %}
- {% if record.genealogy_source %}
- <span title="版本来源"><i class="bi bi-archive"></i> {{ record.genealogy_source }}</span>
- {% endif %}
- </div>
- {% endif %}
- </td>
- <td>
- {% if record.ai_status == 2 %}
- <span class="badge bg-success"><i class="bi bi-check-circle"></i> 解析成功</span>
- {% elif record.ai_status == 1 %}
- <span class="badge bg-warning text-dark"><i class="bi bi-hourglass-split"></i> 解析中...</span>
- {% elif record.ai_status == 3 %}
- <span class="badge bg-danger"><i class="bi bi-x-circle"></i> 解析失败</span>
- {% else %}
- <span class="badge bg-secondary">未解析</span>
- {% endif %}
- </td>
- <td>{{ record.upload_time }}</td>
- <td class="text-center">
- <button onclick="openImageViewer('{{ record.oss_url }}', '{{ record.page_number or '' }}')" class="btn btn-sm btn-outline-info list-preview-btn" data-url="{{ record.oss_url }}" data-page="{{ record.page_number or '' }}" title="查看原图">
- <i class="bi bi-eye"></i>
- </button>
-
- {% if record.ai_status == 2 %}
- <a href="{{ url_for('add_member', record_id=record.id) }}" class="btn btn-sm btn-success" title="查看解析并录入">
- <i class="bi bi-magic"></i> 录入
- </a>
- <button onclick="reStartAiAnalysis({{ record.id }})" class="btn btn-sm btn-outline-warning" title="重新AI解析">
- <i class="bi bi-arrow-repeat"></i> 重析
- </button>
- {% else %}
- <button onclick="startAiAnalysis({{ record.id }})" class="btn btn-sm btn-outline-primary" title="AI 解析">
- <i class="bi bi-robot"></i> 解析
- </button>
- <a href="{{ url_for('add_member', record_id=record.id) }}" class="btn btn-sm btn-outline-secondary" title="手动录入">
- <i class="bi bi-pencil"></i> 手动
- </a>
- {% endif %}
- <form action="{{ url_for('delete_upload', record_id=record.id) }}" method="POST" class="d-inline" onsubmit="return confirm('确定要删除此文件记录吗?此操作无法撤销。');">
- <button type="submit" class="btn btn-sm btn-outline-danger" title="删除">
- <i class="bi bi-trash"></i>
- </button>
- </form>
- </td>
- </tr>
- {% endfor %}
- {% if not records %}
- <tr>
- <td colspan="4" class="text-center py-5 text-muted">
- <i class="bi bi-folder-x fs-1 d-block mb-2"></i>
- 暂无扫描件数据,请先上传。
- </td>
- </tr>
- {% endif %}
- </tbody>
- </table>
- </div>
- </div>
- {% if total_pages > 1 or total > 0 %}
- <div class="card-footer d-flex justify-content-between align-items-center bg-white border-top-0 pt-3">
- <div class="text-muted small">
- 共 <strong>{{ total }}</strong> 条记录,当前第 {{ page }} / {{ total_pages if total_pages > 0 else 1 }} 页
- </div>
- {% if total_pages > 1 %}
- <nav>
- <ul class="pagination pagination-sm mb-0">
- <li class="page-item {% if page == 1 %}disabled{% endif %}">
- <a class="page-link" href="{{ url_for('index', page=page-1, version=version, source=source, person=person, file_type=file_type) }}">上一页</a>
- </li>
-
- {% set start_page = page - 2 if page - 2 > 0 else 1 %}
- {% set end_page = start_page + 4 if start_page + 4 <= total_pages else total_pages %}
- {% set start_page = end_page - 4 if end_page - 4 > 0 else 1 %}
-
- {% for p in range(start_page, end_page + 1) %}
- <li class="page-item {% if p == page %}active{% endif %}">
- <a class="page-link" href="{{ url_for('index', page=p, version=version, source=source, person=person, file_type=file_type) }}">{{ p }}</a>
- </li>
- {% endfor %}
-
- <li class="page-item {% if page == total_pages %}disabled{% endif %}">
- <a class="page-link" href="{{ url_for('index', page=page+1, version=version, source=source, person=person, file_type=file_type) }}">下一页</a>
- </li>
- </ul>
- </nav>
- {% endif %}
- </div>
- {% endif %}
- </div>
- <!-- Image Viewer Modal -->
- <div class="modal fade" id="imageModal" tabindex="-1" aria-hidden="true">
- <div class="modal-dialog modal-fullscreen">
- <div class="modal-content bg-light">
- <div class="modal-header py-2">
- <h5 class="modal-title fs-6"><i class="bi bi-image"></i> 扫描件预览 <span id="modalPageNum" class="badge bg-secondary ms-2"></span></h5>
- <div class="ms-auto me-3 text-muted small" id="listImageIndexIndicator"></div>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body p-0 modal-image-container position-relative">
- <div class="image-toolbar">
- <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>
- <button type="button" class="btn btn-outline-secondary" onclick="rotateImage(90)" title="右旋90°"><i class="bi bi-arrow-clockwise"></i></button>
- </div>
- <div class="d-flex align-items-center gap-2 mx-3 border-start border-end px-3">
- <i class="bi bi-brightness-high" title="亮度"></i>
- <input type="range" class="form-range" min="50" max="150" value="100" id="brightnessRange" oninput="updateImageFilter()" style="width: 80px;">
- <i class="bi bi-circle-half ms-2" title="对比度"></i>
- <input type="range" class="form-range" min="50" max="200" value="100" id="contrastRange" oninput="updateImageFilter()" style="width: 80px;">
- <button class="btn btn-link btn-sm text-decoration-none py-0" onclick="resetFilters()">重置</button>
- </div>
- <div class="form-check form-switch ms-auto mb-0" title="开启后鼠标悬停图片可局部放大">
- <input class="form-check-input" type="checkbox" id="magnifierSwitch">
- <label class="form-check-label small" for="magnifierSwitch">🔍 放大镜</label>
- </div>
- </div>
-
- <button class="btn btn-dark position-absolute top-50 start-0 translate-middle-y ms-3 rounded-circle shadow" id="listPrevImageBtn" style="width: 48px; height: 48px; opacity: 0.7; z-index: 1050; display: none;">
- <i class="bi bi-chevron-left fs-4"></i>
- </button>
- <button class="btn btn-dark position-absolute top-50 end-0 translate-middle-y me-3 rounded-circle shadow" id="listNextImageBtn" style="width: 48px; height: 48px; opacity: 0.7; z-index: 1050; display: none;">
- <i class="bi bi-chevron-right fs-4"></i>
- </button>
- <div class="image-viewer shadow-inner" id="viewer">
- <div id="magnifier" class="magnifier-glass"></div>
- <div id="imageWrapper" class="image-wrapper">
- <img id="refImage" src="" alt="家谱图片" draggable="false">
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- {% endblock %}
- {% block extra_js %}
- <script>
- // --- Image Viewer Logic ---
- let imgRotation = 0;
- let imgBrightness = 100;
- let imgContrast = 100;
- let isDragging = false;
- let hasDragged = false;
- let startX = 0, startY = 0;
- let currentX = 0, currentY = 0;
- let isZoomedIn = false;
- const ZOOM_LEVEL = 2.0;
- let currentListImages = [];
- let currentListIndex = 0;
- document.addEventListener('DOMContentLoaded', () => {
- // Collect all previewable images from the current table
- const imgs = document.querySelectorAll('.list-preview-img');
- imgs.forEach((img, idx) => {
- currentListImages.push({
- url: img.dataset.url,
- page: img.dataset.page
- });
- });
-
- // Add click events to buttons
- document.getElementById('listPrevImageBtn').addEventListener('click', () => {
- if (currentListIndex > 0) openListImage(currentListIndex - 1);
- });
-
- document.getElementById('listNextImageBtn').addEventListener('click', () => {
- if (currentListIndex < currentListImages.length - 1) openListImage(currentListIndex + 1);
- });
-
- // Keyboard navigation
- document.getElementById('imageModal').addEventListener('keydown', (e) => {
- if (e.key === 'ArrowLeft' && currentListIndex > 0) {
- openListImage(currentListIndex - 1);
- } else if (e.key === 'ArrowRight' && currentListIndex < currentListImages.length - 1) {
- openListImage(currentListIndex + 1);
- }
- });
- });
- function openListImage(index) {
- if (index < 0 || index >= currentListImages.length) return;
- currentListIndex = index;
- const data = currentListImages[index];
- openImageViewer(data.url, data.page);
- }
- function openImageViewer(url, pageNum) {
- // Update index if opened via direct click instead of arrows
- const foundIdx = currentListImages.findIndex(img => img.url === url);
- if (foundIdx !== -1) currentListIndex = foundIdx;
-
- const modalEl = document.getElementById('imageModal');
- let modal = bootstrap.Modal.getInstance(modalEl);
- if (!modal) {
- modal = new bootstrap.Modal(modalEl);
- }
-
- document.getElementById('refImage').src = url;
- document.getElementById('modalPageNum').textContent = pageNum ? '第 ' + pageNum + ' 页' : '';
-
- // Update indicator and buttons
- if (currentListImages.length > 0 && foundIdx !== -1) {
- document.getElementById('listImageIndexIndicator').textContent = `${currentListIndex + 1} / ${currentListImages.length}`;
- document.getElementById('listPrevImageBtn').style.display = currentListIndex === 0 ? 'none' : 'block';
- document.getElementById('listNextImageBtn').style.display = currentListIndex === currentListImages.length - 1 ? 'none' : 'block';
- } else {
- document.getElementById('listImageIndexIndicator').textContent = '';
- document.getElementById('listPrevImageBtn').style.display = 'none';
- document.getElementById('listNextImageBtn').style.display = 'none';
- }
- modal.show();
- resetFilters();
- }
- const viewer = document.getElementById('viewer');
- const magnifier = document.getElementById('magnifier');
- const magnifierSwitch = document.getElementById('magnifierSwitch');
- const imageWrapper = document.getElementById('imageWrapper');
- const refImage = document.getElementById('refImage');
-
- if (imageWrapper) {
- viewer.style.cursor = 'zoom-in';
- viewer.addEventListener('mousedown', (e) => {
- if (e.target.closest('.image-toolbar') || e.target.closest('.magnifier-glass')) return;
- isDragging = true;
- hasDragged = false;
- startX = e.clientX;
- startY = e.clientY;
- viewer.style.cursor = 'grabbing';
- e.preventDefault();
- });
-
- window.addEventListener('mousemove', (e) => {
- if (!isDragging) return;
- const dx = e.clientX - startX;
- const dy = e.clientY - startY;
- if (Math.abs(dx) > 2 || Math.abs(dy) > 2) hasDragged = true;
- currentX += dx;
- currentY += dy;
- startX = e.clientX;
- startY = e.clientY;
- updateImageTransform();
- });
-
- window.addEventListener('mouseup', (e) => {
- if (isDragging) {
- isDragging = false;
- viewer.style.cursor = isZoomedIn ? 'grab' : 'zoom-in';
- if (!hasDragged && viewer.contains(e.target)) toggleZoom();
- }
- });
- }
- function toggleZoom() {
- isZoomedIn = !isZoomedIn;
- if (!isZoomedIn) {
- currentX = 0;
- currentY = 0;
- }
- updateImageTransform();
- viewer.style.cursor = isZoomedIn ? 'grab' : 'zoom-in';
- }
-
- viewer.addEventListener('mousemove', function(e) {
- if (!magnifierSwitch.checked || isDragging || !refImage.src) {
- magnifier.style.display = 'none';
- return;
- }
- const rect = refImage.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
-
- if (x < 0 || x > rect.width || y < 0 || y > rect.height) {
- magnifier.style.display = 'none';
- return;
- }
- magnifier.style.display = 'block';
- const glassOffset = 20;
- const viewerRect = viewer.getBoundingClientRect();
- magnifier.style.left = (e.clientX - viewerRect.left + glassOffset) + 'px';
- magnifier.style.top = (e.clientY - viewerRect.top + glassOffset) + 'px';
-
- const zoom = 2.5;
- magnifier.style.backgroundImage = `url('${refImage.src}')`;
- magnifier.style.backgroundSize = `${rect.width * zoom}px ${rect.height * zoom}px`;
- magnifier.style.backgroundPosition = `-${x * zoom - 75}px -${y * zoom - 75}px`;
- });
- function rotateImage(deg) {
- imgRotation = (imgRotation + deg) % 360;
- updateImageTransform();
- }
-
- function updateImageFilter() {
- imgBrightness = document.getElementById('brightnessRange').value;
- imgContrast = document.getElementById('contrastRange').value;
- applyImageFilters();
- }
-
- function resetFilters() {
- imgRotation = 0;
- imgBrightness = 100;
- imgContrast = 100;
- currentX = 0;
- currentY = 0;
- isZoomedIn = false;
- document.getElementById('brightnessRange').value = 100;
- document.getElementById('contrastRange').value = 100;
- if(magnifierSwitch) magnifierSwitch.checked = false;
- updateImageTransform();
- applyImageFilters();
- if(viewer) viewer.style.cursor = 'zoom-in';
- }
-
- function updateImageTransform() {
- if (imageWrapper) {
- const scale = isZoomedIn ? ZOOM_LEVEL : 1;
- imageWrapper.style.transform = `translate(calc(-50% + ${currentX}px), calc(-50% + ${currentY}px)) rotate(${imgRotation}deg) scale(${scale})`;
- }
- }
-
- function applyImageFilters() {
- if (refImage) {
- refImage.style.filter = `brightness(${imgBrightness}%) contrast(${imgContrast}%)`;
- }
- }
- function startAiAnalysis(recordId) {
- if (!confirm('确定要开始 AI 解析吗?这可能需要一些时间。')) return;
-
- // Optimistic UI update
- const btn = event.currentTarget;
- const originalHtml = btn.innerHTML;
- btn.disabled = true;
- btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 解析中...';
-
- fetch('/manager/api/start_analysis/' + recordId, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- }
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- // Reload to show status update (or update DOM dynamically)
- window.location.reload();
- } else {
- alert('启动解析失败: ' + data.message);
- btn.innerHTML = originalHtml;
- btn.disabled = false;
- }
- })
- .catch(error => {
- console.error('Error:', error);
- alert('请求失败,请重试');
- btn.innerHTML = originalHtml;
- btn.disabled = false;
- });
- }
- function reStartAiAnalysis(recordId) {
- if (!confirm('确定要重新进行 AI 解析吗?这将覆盖原有的解析结果。')) return;
-
- const btn = event.currentTarget;
- const originalHtml = btn.innerHTML;
- btn.disabled = true;
- btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 解析中...';
-
- fetch('/manager/api/start_analysis/' + recordId, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- }
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- window.location.reload();
- } else {
- alert('启动解析失败: ' + data.message);
- btn.innerHTML = originalHtml;
- btn.disabled = false;
- }
- })
- .catch(error => {
- console.error('Error:', error);
- alert('请求失败,请重试');
- btn.innerHTML = originalHtml;
- btn.disabled = false;
- });
- }
- </script>
- {% endblock %}
|