Browse Source

commit 优化样式

Hai Lin 3 weeks ago
parent
commit
39ecf165f4
5 changed files with 504 additions and 122 deletions
  1. 189 29
      app.py
  2. 3 1
      templates/index.html
  3. 158 17
      templates/pdf_management.html
  4. 54 75
      templates/upload.html
  5. 100 0
      templates/upload_pdf.html

+ 189 - 29
app.py

@@ -547,9 +547,29 @@ def ensure_pdf_table():
                     oss_url TEXT NOT NULL,
                     description VARCHAR(500) DEFAULT '',
                     upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-                    uploader VARCHAR(100) DEFAULT ''
+                    uploader VARCHAR(100) DEFAULT '',
+                    version_name VARCHAR(255) DEFAULT '',
+                    version_source VARCHAR(255) DEFAULT '',
+                    file_provider VARCHAR(100) DEFAULT '',
+                    parse_status INT DEFAULT 0
                 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
             """)
+            # 检查是否存在parse_status字段,如果不存在则添加
+            cursor.execute("SHOW COLUMNS FROM genealogy_pdfs LIKE 'parse_status'")
+            if not cursor.fetchone():
+                cursor.execute("ALTER TABLE genealogy_pdfs ADD COLUMN parse_status INT DEFAULT 0")
+            # 检查是否存在version_name字段,如果不存在则添加
+            cursor.execute("SHOW COLUMNS FROM genealogy_pdfs LIKE 'version_name'")
+            if not cursor.fetchone():
+                cursor.execute("ALTER TABLE genealogy_pdfs ADD COLUMN version_name VARCHAR(255) DEFAULT ''")
+            # 检查是否存在version_source字段,如果不存在则添加
+            cursor.execute("SHOW COLUMNS FROM genealogy_pdfs LIKE 'version_source'")
+            if not cursor.fetchone():
+                cursor.execute("ALTER TABLE genealogy_pdfs ADD COLUMN version_source VARCHAR(255) DEFAULT ''")
+            # 检查是否存在file_provider字段,如果不存在则添加
+            cursor.execute("SHOW COLUMNS FROM genealogy_pdfs LIKE 'file_provider'")
+            if not cursor.fetchone():
+                cursor.execute("ALTER TABLE genealogy_pdfs ADD COLUMN file_provider VARCHAR(100) DEFAULT ''")
         conn.commit()
     finally:
         conn.close()
@@ -561,6 +581,7 @@ def pdf_management():
 
     ensure_pdf_table()
     view_id = request.args.get('view', type=int)
+    preview = request.args.get('preview', type=bool, default=False)
     selected_pdf = None
 
     conn = get_db_connection()
@@ -568,16 +589,139 @@ def pdf_management():
         with conn.cursor() as cursor:
             cursor.execute("SELECT * FROM genealogy_pdfs ORDER BY upload_time DESC")
             pdfs = cursor.fetchall()
-            if view_id:
+            if view_id and preview:
                 cursor.execute("SELECT * FROM genealogy_pdfs WHERE id = %s", (view_id,))
                 selected_pdf = cursor.fetchone()
-            elif pdfs:
-                selected_pdf = pdfs[0]
     finally:
         conn.close()
 
     return render_template('pdf_management.html', pdfs=pdfs, selected_pdf=selected_pdf)
 
+
+
+@app.route('/manager/parse_pdf/<int:pdf_id>', methods=['POST'])
+def parse_pdf(pdf_id):
+    if 'user_id' not in session:
+        return jsonify({"success": False, "message": "Unauthorized"}), 401
+
+    # 标记PDF为解析中
+    conn = get_db_connection()
+    try:
+        with conn.cursor() as cursor:
+            cursor.execute("UPDATE genealogy_pdfs SET parse_status = 1 WHERE id = %s", (pdf_id,))
+        conn.commit()
+    finally:
+        conn.close()
+
+    # 异步执行PDF解析
+    def parse_pdf_async():
+        try:
+            # 获取PDF信息
+            conn = get_db_connection()
+            pdf_info = None
+            try:
+                with conn.cursor() as cursor:
+                    cursor.execute("SELECT * FROM genealogy_pdfs WHERE id = %s", (pdf_id,))
+                    pdf_info = cursor.fetchone()
+            finally:
+                conn.close()
+
+            if not pdf_info:
+                return
+
+            # 下载PDF并拆分
+            pdf_url = pdf_info['oss_url']
+            response = requests.get(pdf_url)
+            response.raise_for_status()
+
+            # 保存临时PDF文件
+            temp_pdf_path = f"/tmp/{pdf_info['file_name']}"
+            with open(temp_pdf_path, 'wb') as f:
+                f.write(response.content)
+
+            # 使用PyMuPDF拆分PDF
+            doc = fitz.open(temp_pdf_path)
+            page_count = doc.page_count
+
+            # 获取当前最大页码
+            conn = get_db_connection()
+            max_page = 0
+            try:
+                with conn.cursor() as cursor:
+                    cursor.execute("SELECT MAX(page_number) as max_page FROM genealogy_records")
+                    result = cursor.fetchone()
+                    if result and result['max_page']:
+                        max_page = result['max_page']
+            finally:
+                conn.close()
+
+            # 逐页处理
+            for i in range(page_count):
+                page = doc[i]
+                pix = page.get_pixmap()
+                image_path = f"/tmp/{pdf_info['file_name']}_page_{i+1}.png"
+                pix.save(image_path)
+
+                # 上传图片到OSS
+                with open(image_path, 'rb') as f:
+                    image_oss_url = upload_to_oss(f, f"{pdf_info['file_name']}_page_{i+1}.png")
+
+                # 保存到genealogy_records表
+                conn = get_db_connection()
+                try:
+                    with conn.cursor() as cursor:
+                        cursor.execute("""
+                            INSERT INTO genealogy_records 
+                            (file_name, oss_url, file_type, page_number, genealogy_version, genealogy_source, upload_person, upload_time)
+                            VALUES (%s, %s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP)
+                        """, (
+                            f"{pdf_info['file_name']}_page_{i+1}.png",
+                            image_oss_url,
+                            '图片',
+                            max_page + i + 1,
+                            pdf_info['version_name'],
+                            pdf_info['version_source'],
+                            pdf_info['file_provider']
+                        ))
+                    conn.commit()
+                finally:
+                    conn.close()
+
+                # 删除临时图片文件
+                if os.path.exists(image_path):
+                    os.remove(image_path)
+
+            # 删除临时PDF文件
+            if os.path.exists(temp_pdf_path):
+                os.remove(temp_pdf_path)
+
+            # 更新PDF解析状态为成功
+            conn = get_db_connection()
+            try:
+                with conn.cursor() as cursor:
+                    cursor.execute("UPDATE genealogy_pdfs SET parse_status = 2 WHERE id = %s", (pdf_id,))
+                conn.commit()
+            finally:
+                conn.close()
+
+        except Exception as e:
+            # 更新PDF解析状态为失败
+            conn = get_db_connection()
+            try:
+                with conn.cursor() as cursor:
+                    cursor.execute("UPDATE genealogy_pdfs SET parse_status = 3 WHERE id = %s", (pdf_id,))
+                conn.commit()
+            finally:
+                conn.close()
+            print(f"PDF解析失败: {e}")
+
+    # 启动异步任务
+    thread = threading.Thread(target=parse_pdf_async)
+    thread.daemon = True
+    thread.start()
+
+    return jsonify({"success": True, "message": "PDF解析已开始,将在后台执行"})
+
 @app.route('/manager/delete_pdf/<int:pdf_id>', methods=['POST'])
 def delete_pdf(pdf_id):
     if 'user_id' not in session:
@@ -1882,21 +2026,46 @@ def delete_upload(record_id):
     finally:
         conn.close()
 
-@app.route('/manager/upload_pdf', methods=['POST'])
+@app.route('/manager/upload_pdf', methods=['GET', 'POST'])
 def upload_pdf():
     if 'user_id' not in session:
-        return jsonify({"success": False, "message": "Unauthorized"}), 401
+        return redirect(url_for('login'))
     
-    if 'file' not in request.files:
-        return jsonify({"success": False, "message": "未选择文件"}), 400
+    if request.method == 'GET':
+        return render_template('upload_pdf.html')
     
+    # POST请求处理
+    if 'file' not in request.files:
+        flash('请选择要上传的PDF文件')
+        return redirect(request.url)
+
     file = request.files['file']
     if file.filename == '':
-        return jsonify({"success": False, "message": "未选择文件"}), 400
-    
+        flash('请选择要上传的PDF文件')
+        return redirect(request.url)
+
+    # 检查文件类型
     if not file.filename.lower().endswith('.pdf'):
-        return jsonify({"success": False, "message": "请上传PDF文件"}), 400
-    
+        flash('只支持PDF文件上传')
+        return redirect(request.url)
+
+    # 获取表单数据
+    version_name = request.form.get('version_name', '').strip()
+    version_source = request.form.get('version_source', '').strip()
+    file_provider = request.form.get('file_provider', '').strip()
+
+    # 验证必填字段
+    if not version_name:
+        flash('版本名称为必填项')
+        return redirect(request.url)
+    if not version_source:
+        flash('版本来源为必填项')
+        return redirect(request.url)
+
+    # 如果未提供文件提供人,使用当前登录用户
+    if not file_provider:
+        file_provider = session.get('user_id', '未知')
+
     import uuid
     original_filename = file.filename
     ext = os.path.splitext(original_filename)[1].lower()
@@ -1917,33 +2086,24 @@ def upload_pdf():
         # Upload to OSS
         oss_url = upload_to_oss(file_path, custom_filename=filename)
         if not oss_url:
-            return jsonify({"success": False, "message": "文件上传失败"}), 500
-        
-        # Get form data
-        uploader = request.form.get('uploader', session.get('username', ''))
-        version_name = request.form.get('version_name', '')
-        version_source = request.form.get('version_source', '')
-        file_provider = request.form.get('file_provider', uploader)
+            flash('文件上传失败')
+            return redirect(request.url)
         
         # Save to database
         conn = get_db_connection()
         try:
             with conn.cursor() as cursor:
                 cursor.execute(
-                    "INSERT INTO genealogy_pdfs (file_name, oss_url, uploader, version_name, version_source, file_provider) VALUES (%s, %s, %s, %s, %s, %s)",
-                    (original_filename, oss_url, uploader, version_name, version_source, file_provider)
+                    "INSERT INTO genealogy_pdfs (file_name, oss_url, version_name, version_source, file_provider, upload_time) VALUES (%s, %s, %s, %s, %s, CURRENT_TIMESTAMP)",
+                    (original_filename, oss_url, version_name, version_source, file_provider)
                 )
             conn.commit()
             
-            # Start background processing for PDF pages
-            threading.Thread(
-                target=process_pdf_pages,
-                args=(file_path, oss_url, uploader)
-            ).start()
-            
-            return jsonify({"success": True, "message": "PDF文件上传成功,正在解析页面"})
+            flash('PDF文件上传成功')
+            return redirect(url_for('pdf_management'))
         except Exception as e:
-            return jsonify({"success": False, "message": f"保存失败: {e}"}), 500
+            flash(f'保存失败: {e}')
+            return redirect(request.url)
         finally:
             conn.close()
     finally:

+ 3 - 1
templates/index.html

@@ -87,7 +87,9 @@
 {% 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-2"></i> 上传扫描件
+    </a>
 </div>
 
 <div class="card shadow-sm mb-4">

+ 158 - 17
templates/pdf_management.html

@@ -8,34 +8,49 @@
         cursor: pointer;
         transition: all 0.3s ease;
         border: 2px solid transparent;
-        border-radius: 8px;
+        border-radius: 12px;
+        overflow: hidden;
+        background-color: #ffffff;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+        min-height: 280px;
+        display: flex;
+        flex-direction: column;
     }
     .pdf-card:hover {
         border-color: #0d6efd;
-        box-shadow: 0 4px 16px rgba(13,110,253,0.18);
-        transform: translateY(-2px);
+        box-shadow: 0 6px 20px rgba(13, 110, 253, 0.2);
+        transform: translateY(-3px);
     }
     .pdf-card.active {
         border-color: #0d6efd;
         background-color: #f0f6ff;
+        box-shadow: 0 4px 16px rgba(13, 110, 253, 0.18);
     }
     .pdf-card .card-body {
         padding: 16px;
+        flex: 1;
+        display: flex;
+        flex-direction: column;
     }
     .pdf-card-icon {
         font-size: 1.8rem;
         color: #dc3545;
+        margin-top: 2px;
     }
     .pdf-card-title {
-        font-size: 0.95rem;
+        font-size: 0.9rem;
         font-weight: 600;
         line-height: 1.3;
-        margin-bottom: 8px;
+        margin-bottom: 10px;
+        color: #333333;
+        flex-shrink: 0;
     }
     .pdf-card-meta {
         font-size: 0.75rem;
         color: #6c757d;
         line-height: 1.4;
+        margin-bottom: 12px;
+        flex-grow: 1;
     }
     .pdf-card-meta-item {
         display: flex;
@@ -47,13 +62,15 @@
         margin-right: 6px;
         width: 14px;
         text-align: center;
+        color: #adb5bd;
     }
     .pdf-viewer-wrapper {
-        background: #e9ecef;
-        border-radius: 8px;
+        background: #f8f9fa;
+        border-radius: 12px;
         overflow: hidden;
         min-height: 80vh;
         position: relative;
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
     }
     .pdf-viewer-wrapper iframe,
     .pdf-viewer-wrapper embed {
@@ -62,25 +79,64 @@
         border: none;
     }
     .pdf-list-scroll {
-        max-height: 300px;
+        max-height: 600px;
         overflow-y: auto;
+        padding-right: 8px;
+    }
+    .pdf-list-scroll::-webkit-scrollbar {
+        width: 6px;
+    }
+    .pdf-list-scroll::-webkit-scrollbar-track {
+        background: #f1f1f1;
+        border-radius: 3px;
+    }
+    .pdf-list-scroll::-webkit-scrollbar-thumb {
+        background: #c1c1c1;
+        border-radius: 3px;
+    }
+    .pdf-list-scroll::-webkit-scrollbar-thumb:hover {
+        background: #a1a1a1;
     }
     .pdf-detail-meta {
         display: flex;
         flex-wrap: wrap;
-        gap: 16px;
-        margin-top: 8px;
-        padding-top: 12px;
+        gap: 20px;
+        margin-top: 12px;
+        padding-top: 16px;
         border-top: 1px solid #e9ecef;
     }
     .pdf-detail-meta-item {
         display: flex;
         align-items: center;
-        font-size: 0.85rem;
+        font-size: 0.9rem;
     }
     .pdf-detail-meta-item i {
         color: #6c757d;
-        margin-right: 8px;
+        margin-right: 10px;
+        font-size: 1rem;
+    }
+    .card {
+        border-radius: 12px;
+        overflow: hidden;
+    }
+    .card-header {
+        border-bottom: none;
+        background-color: #ffffff;
+    }
+    .card-body {
+        padding: 24px;
+    }
+    .btn {
+        border-radius: 8px;
+        font-weight: 500;
+    }
+    .btn-sm {
+        padding: 6px 12px;
+        font-size: 0.875rem;
+    }
+    .badge {
+        font-weight: 500;
+        font-size: 0.75rem;
     }
     .loading-overlay {
         position: absolute;
@@ -136,6 +192,9 @@
 {% block content %}
 <div class="d-flex justify-content-between align-items-center mb-4">
     <h2><i class="bi bi-book"></i> 家谱管理</h2>
+    <a href="{{ url_for('upload_pdf') }}" class="btn btn-primary">
+        <i class="bi bi-cloud-upload me-2"></i> 上传家谱
+    </a>
 </div>
 
 {% if pdfs %}
@@ -144,12 +203,32 @@
         <span class="fw-bold small text-muted">已上传家谱文件({{ pdfs|length }} 个)</span>
     </div>
     <div class="card-body p-2 pdf-list-scroll">
-        <div class="row g-2">
+        <div class="row g-4">
             {% for pdf in pdfs %}
-            <div class="col-md-3 col-sm-6">
+            <div class="col-md-4 col-sm-6">
                 <div class="card pdf-card {{ 'active' if selected_pdf and selected_pdf.id == pdf.id }}"
-                     onclick="window.location.href='{{ url_for('pdf_management', view=pdf.id) }}'">
-                    <div class="card-body">
+                     onclick="window.location.href='{{ url_for('pdf_management', view=pdf.id) }}'>
+                    <div class="position-relative">
+                        <div class="position-absolute top-2 right-2 z-10">
+                            {% if pdf.parse_status == 2 %}
+                            <span class="badge bg-success rounded-full p-2">
+                                <i class="bi bi-check-circle"></i> 解析成功
+                            </span>
+                            {% elif pdf.parse_status == 1 %}
+                            <span class="badge bg-warning text-dark rounded-full p-2">
+                                <i class="bi bi-hourglass-split"></i> 解析中
+                            </span>
+                            {% elif pdf.parse_status == 3 %}
+                            <span class="badge bg-danger rounded-full p-2">
+                                <i class="bi bi-x-circle"></i> 解析失败
+                            </span>
+                            {% else %}
+                            <span class="badge bg-secondary rounded-full p-2">
+                                <i class="bi bi-robot"></i> 未解析
+                            </span>
+                            {% endif %}
+                        </div>
+                        <div class="card-body">
                         <div class="d-flex align-items-start">
                             <i class="bi bi-file-earmark-pdf pdf-card-icon me-3 flex-shrink-0"></i>
                             <div class="flex-grow-1">
@@ -178,6 +257,24 @@
                                         <span>{{ pdf.upload_time.strftime('%Y-%m-%d') if pdf.upload_time else '未知' }}</span>
                                     </div>
                                 </div>
+                                <div class="mt-3 d-grid gap-2">
+                                    <button onclick="previewPDF({{ pdf.id }})" class="btn btn-sm btn-outline-info w-100" id="previewBtn{{ pdf.id }}">
+                                        <i class="bi bi-eye"></i> 预览
+                                    </button>
+                                    {% if pdf.parse_status == 2 %}
+                                    <button onclick="parsePDF({{ pdf.id }})" class="btn btn-sm btn-outline-warning w-100" id="parseBtn{{ pdf.id }}">
+                                        <i class="bi bi-arrow-repeat"></i> 重新解析
+                                    </button>
+                                    {% elif pdf.parse_status != 1 %}
+                                    <button onclick="parsePDF({{ pdf.id }})" class="btn btn-sm btn-outline-primary w-100" id="parseBtn{{ pdf.id }}">
+                                        <i class="bi bi-robot"></i> AI解析
+                                    </button>
+                                    {% else %}
+                                    <button class="btn btn-sm btn-outline-primary w-100 disabled">
+                                        <span class="spinner-border spinner-border-sm"></span> 解析中...
+                                    </button>
+                                    {% endif %}
+                                </div>
                             </div>
                         </div>
                     </div>
@@ -331,5 +428,49 @@
             }, 200);
         });
     });
+    
+    // PDF预览函数
+    function previewPDF(pdfId) {
+        window.location.href = '{{ url_for('pdf_management') }}?view=' + pdfId + '&preview=true';
+    }
+    
+    // PDF解析函数
+    function parsePDF(pdfId) {
+        if (!confirm('确定要开始AI解析吗?这将把PDF拆分为单页并生成扫描件数据,可能需要一些时间。')) {
+            return;
+        }
+        
+        const btn = document.getElementById('parseBtn' + pdfId);
+        const originalHtml = btn.innerHTML;
+        btn.disabled = true;
+        btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 解析中...';
+        
+        fetch('/manager/parse_pdf/' + pdfId, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            }
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                alert('PDF解析已开始,将在后台执行。解析完成后状态会自动更新。');
+                // 重新加载页面以更新状态
+                setTimeout(() => {
+                    window.location.reload();
+                }, 1000);
+            } 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 %}

+ 54 - 75
templates/upload.html

@@ -19,9 +19,9 @@
                 <form method="POST" enctype="multipart/form-data" id="uploadForm">
                     <div class="mb-4">
                         <label class="form-label fw-bold">选择文件</label>
-                        <input type="file" name="file" id="fileInput" class="form-control form-control-lg" multiple accept=".jpg,.jpeg,.png,.pdf" required>
+                        <input type="file" name="file" id="fileInput" class="form-control form-control-lg" multiple accept=".jpg,.jpeg,.png" required>
                         <div class="form-text mt-2">
-                            支持图片 (JPG, PNG) 或 PDF 格式的扫描件。图片支持多选(一次最多10张)。PDF文件会自动按页拆分提取。上传后将自动识别或提取页码。
+                            支持图片 (JPG, PNG) 格式的扫描件。图片支持多选(一次最多10张)。上传后将自动识别或提取页码。
                         </div>
                     </div>
 
@@ -184,14 +184,17 @@
 
     document.getElementById('fileInput').addEventListener('change', function(e) {
         let files = e.target.files;
-        let pdfCount = 0;
         let imgCount = 0;
 
         for (let i = 0; i < files.length; i++) {
-            if (files[i].type === 'application/pdf' || files[i].name.toLowerCase().endsWith('.pdf')) {
-                pdfCount++;
-            } else {
+            if (files[i].type.startsWith('image/')) {
                 imgCount++;
+            } else if (files[i].name.toLowerCase().endsWith('.pdf')) {
+                alert('扫描件管理仅支持图片上传,请在家谱管理中上传PDF文件。');
+                e.target.value = ''; // clear selection
+                document.getElementById('previewContainer').style.display = 'none';
+                document.getElementById('globalPageWrapper').style.display = 'block';
+                return;
             }
         }
 
@@ -201,12 +204,6 @@
             document.getElementById('previewContainer').style.display = 'none';
             document.getElementById('globalPageWrapper').style.display = 'block';
             return;
-        } else if (pdfCount > 0 && files.length > 1) {
-            alert('上传PDF时,一次只能选择1个文件。');
-            e.target.value = ''; // clear selection
-            document.getElementById('previewContainer').style.display = 'none';
-            document.getElementById('globalPageWrapper').style.display = 'block';
-            return;
         }
         
         const previewContainer = document.getElementById('previewContainer');
@@ -217,77 +214,59 @@
         
         if (files.length > 0) {
             previewContainer.style.display = 'block';
-            if (imgCount > 0) {
-                // Hide global page input for images, as we will set them individually
-                globalPageWrapper.style.display = 'none';
-                
-                let startPage = parseInt('{{ suggested_page }}') || 1;
-                
-                currentPreviewFiles = []; // Reset current preview files
-                
-                Array.from(files).forEach((file, index) => {
-                    if (file.type.startsWith('image/') || !file.name.toLowerCase().endsWith('.pdf')) {
-                        currentPreviewFiles.push(file);
-                    }
-                });
-                
-                let imageIndexCounter = 0;
+            // Hide global page input for images, as we will set them individually
+            globalPageWrapper.style.display = 'none';
+            
+            let startPage = parseInt('{{ suggested_page }}') || 1;
+            
+            currentPreviewFiles = []; // Reset current preview files
+            
+            Array.from(files).forEach((file, index) => {
+                if (file.type.startsWith('image/')) {
+                    currentPreviewFiles.push(file);
+                }
+            });
+            
+            let imageIndexCounter = 0;
 
-                Array.from(files).forEach((file, index) => {
-                    const isImage = file.type.startsWith('image/') || !file.name.toLowerCase().endsWith('.pdf');
-                    if (!isImage) return; // Skip non-images
-                    
-                    const currentIndex = imageIndexCounter++;
-                    const objectUrl = URL.createObjectURL(file);
-                    const col = document.createElement('div');
-                    col.className = 'col-md-6 col-lg-4';
-                    col.innerHTML = `
-                        <div class="card h-100">
-                            <div class="position-relative preview-click-area" data-index="${currentIndex}" style="cursor: pointer;">
-                                <img src="${objectUrl}" class="card-img-top" style="height: 200px; object-fit: cover; background: #f8f9fa; transition: opacity 0.2s;" alt="预览图" onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
-                                <div class="position-absolute top-50 start-50 translate-middle text-white" style="opacity: 0; transition: opacity 0.2s; pointer-events: none;" onmouseover="this.parentElement.querySelector('img').style.opacity='0.8'; this.style.opacity='1'" onmouseout="this.parentElement.querySelector('img').style.opacity='1'; this.style.opacity='0'">
-                                    <i class="bi bi-zoom-in fs-1 drop-shadow"></i>
-                                </div>
-                            </div>
-                            <div class="card-body p-2 border-top">
-                                <div class="text-truncate small mb-2 text-muted" title="${file.name}">${file.name}</div>
-                                <div class="input-group input-group-sm">
-                                    <span class="input-group-text bg-light text-secondary">指定页码</span>
-                                    <input type="number" name="page_number_${index}" class="form-control" value="${startPage + currentIndex}">
-                                </div>
-                            </div>
-                        </div>
-                    `;
-                    
-                    // Bind click event safely
-                    const clickArea = col.querySelector('.preview-click-area');
-                    if (clickArea) {
-                        clickArea.addEventListener('click', function(evt) {
-                            evt.preventDefault();
-                            evt.stopPropagation();
-                            window.showLargePreview(currentIndex);
-                        });
-                    }
-                    
-                    previewGrid.appendChild(col);
-                });
-            } else if (pdfCount > 0) {
-                // For PDF, keep the global page input
-                globalPageWrapper.style.display = 'block';
+            Array.from(files).forEach((file, index) => {
+                const isImage = file.type.startsWith('image/');
+                if (!isImage) return; // Skip non-images
                 
+                const currentIndex = imageIndexCounter++;
+                const objectUrl = URL.createObjectURL(file);
                 const col = document.createElement('div');
-                col.className = 'col-12';
+                col.className = 'col-md-6 col-lg-4';
                 col.innerHTML = `
-                    <div class="alert alert-secondary d-flex align-items-center mb-0">
-                        <i class="bi bi-file-earmark-pdf fs-3 me-3 text-danger"></i>
-                        <div>
-                            <strong>${files[0].name}</strong><br>
-                            <span class="small text-muted">PDF文件将自动按页拆分处理</span>
+                    <div class="card h-100">
+                        <div class="position-relative preview-click-area" data-index="${currentIndex}" style="cursor: pointer;">
+                            <img src="${objectUrl}" class="card-img-top" style="height: 200px; object-fit: cover; background: #f8f9fa; transition: opacity 0.2s;" alt="预览图" onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
+                            <div class="position-absolute top-50 start-50 translate-middle text-white" style="opacity: 0; transition: opacity 0.2s; pointer-events: none;" onmouseover="this.parentElement.querySelector('img').style.opacity='0.8'; this.style.opacity='1'" onmouseout="this.parentElement.querySelector('img').style.opacity='1'; this.style.opacity='0'">
+                                <i class="bi bi-zoom-in fs-1 drop-shadow"></i>
+                            </div>
+                        </div>
+                        <div class="card-body p-2 border-top">
+                            <div class="text-truncate small mb-2 text-muted" title="${file.name}">${file.name}</div>
+                            <div class="input-group input-group-sm">
+                                <span class="input-group-text bg-light text-secondary">指定页码</span>
+                                <input type="number" name="page_number_${index}" class="form-control" value="${startPage + currentIndex}">
+                            </div>
                         </div>
                     </div>
                 `;
+                
+                // Bind click event safely
+                const clickArea = col.querySelector('.preview-click-area');
+                if (clickArea) {
+                    clickArea.addEventListener('click', function(evt) {
+                        evt.preventDefault();
+                        evt.stopPropagation();
+                        window.showLargePreview(currentIndex);
+                    });
+                }
+                
                 previewGrid.appendChild(col);
-            }
+            });
         } else {
             previewContainer.style.display = 'none';
             globalPageWrapper.style.display = 'block';

+ 100 - 0
templates/upload_pdf.html

@@ -0,0 +1,100 @@
+{% extends "layout.html" %}
+
+{% block title %}上传家谱 - 家谱管理系统{% endblock %}
+
+{% block extra_css %}
+<style>
+    .drop-shadow { filter: drop-shadow(0 0 4px rgba(0,0,0,0.5)); }
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="row justify-content-center">
+    <div class="col-md-8">
+        <div class="card shadow">
+            <div class="card-header bg-primary text-white">
+                <h5 class="mb-0"><i class="bi bi-cloud-upload me-2"></i>上传家谱PDF文件</h5>
+            </div>
+            <div class="card-body p-4">
+                <form method="POST" enctype="multipart/form-data" id="uploadForm">
+                    <div class="mb-4">
+                        <label class="form-label fw-bold">选择PDF文件</label>
+                        <input type="file" name="file" id="fileInput" class="form-control form-control-lg" accept=".pdf" required>
+                        <div class="form-text mt-2">
+                            只支持PDF格式的家谱文件。上传后将显示在家谱管理列表中。
+                        </div>
+                    </div>
+
+                    <div class="row mb-4">
+                        <div class="col-md-4">
+                            <label class="form-label fw-bold">版本名称 <span class="text-danger">*</span></label>
+                            <input type="text" name="version_name" class="form-control" placeholder="如:衢州1926版" required>
+                        </div>
+                        <div class="col-md-4">
+                            <label class="form-label fw-bold">版本来源 <span class="text-danger">*</span></label>
+                            <input type="text" name="version_source" class="form-control" placeholder="如:留越收藏" required>
+                        </div>
+                        <div class="col-md-4">
+                            <label class="form-label fw-bold">文件提供人</label>
+                            <input type="text" name="file_provider" class="form-control" placeholder="默认:当前系统登录账号">
+                        </div>
+                    </div>
+
+                    <div class="alert alert-warning mb-4">
+                        <i class="bi bi-info-circle me-2"></i>
+                        提示:文件将上传至云端 OSS 存储,处理过程可能需要几秒钟。
+                    </div>
+
+                    <div class="d-grid gap-2 d-md-flex justify-content-md-end">
+                        <a href="{{ url_for('pdf_management') }}" class="btn btn-light px-4">取消</a>
+                        <button type="submit" class="btn btn-primary px-5" id="submitBtn">
+                            <i class="bi bi-check-lg me-1"></i> 开始上传
+                        </button>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}
+
+{% block extra_js %}
+<script>
+    document.getElementById('uploadForm').addEventListener('submit', function(e) {
+        // Prevent multiple submissions
+        const btn = document.getElementById('submitBtn');
+        if (btn.classList.contains('disabled')) {
+            e.preventDefault();
+            return;
+        }
+        
+        // Show loading state without blocking submit
+        btn.classList.add('disabled');
+        btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 上传处理中...';
+        
+        // Disable after a tiny delay to ensure form submission proceeds
+        setTimeout(() => {
+            btn.disabled = true;
+        }, 10);
+    });
+
+    document.getElementById('fileInput').addEventListener('change', function(e) {
+        let files = e.target.files;
+        
+        if (files.length > 1) {
+            alert('一次只能上传1个PDF文件。');
+            e.target.value = ''; // clear selection
+            return;
+        }
+        
+        if (files.length > 0) {
+            const file = files[0];
+            if (!file.name.toLowerCase().endsWith('.pdf')) {
+                alert('只支持PDF文件上传。');
+                e.target.value = ''; // clear selection
+                return;
+            }
+        }
+    });
+</script>
+{% endblock %}