Переглянути джерело

commit 上传文件逻辑优化

Hai Lin 1 тиждень тому
батько
коміт
02db68dec2

+ 346 - 57
app.py

@@ -5,6 +5,7 @@ import json
 import re
 import threading
 import urllib3
+import fitz  # PyMuPDF
 from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify, Response, stream_with_context
 from werkzeug.utils import secure_filename
 from oss_utils import upload_to_oss
@@ -22,15 +23,40 @@ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
 
 # 数据库配置
 DB_CONFIG = {
-    "host": "rm-f8ze60yirdj8786u2.mysql.rds.aliyuncs.com",
+    "host": "rm-f8ze60yirdj8786u2wo.mysql.rds.aliyuncs.com",
     "port": 3306,
-    "user": "root",
-    "password": "csqz@20255",
+    "user": "csqz",
+    "password": "csqz@2026",
     "db": "csqz-client",
     "charset": "utf8mb4",
     "cursorclass": pymysql.cursors.DictCursor
 }
 
+from PIL import Image
+
+def compress_image_if_needed(file_path, max_dim=2000):
+    """Compress, resize and normalize image to JPEG for AI processing."""
+    try:
+        # We always want to normalize to JPEG so AI doesn't complain about format
+        with Image.open(file_path) as img:
+            # Convert RGBA/P or any other mode to RGB for JPEG saving
+            if img.mode != 'RGB':
+                img = img.convert('RGB')
+                
+            width, height = img.size
+            if max(width, height) > max_dim:
+                ratio = max_dim / max(width, height)
+                new_size = (int(width * ratio), int(height * ratio))
+                img = img.resize(new_size, Image.Resampling.LANCZOS)
+                
+            # Always save as JPEG to normalize the format
+            new_path = os.path.splitext(file_path)[0] + '_normalized.jpg'
+            img.save(new_path, 'JPEG', quality=85)
+            return new_path
+    except Exception as e:
+        print(f"Warning: Image compression/normalization failed for {file_path}: {e}")
+        return file_path
+
 def get_db_connection():
     return pymysql.connect(**DB_CONFIG)
 
@@ -100,6 +126,39 @@ def clean_name(name):
         
     return name
 
+def get_normalized_base64_image(image_url):
+    """Download image, normalize to JPEG, and return base64 data URI for AI payload."""
+    import io
+    import base64
+    import requests
+    from PIL import Image
+    
+    try:
+        response = requests.get(image_url, timeout=30)
+        response.raise_for_status()
+        
+        with Image.open(io.BytesIO(response.content)) as img:
+            # Convert to RGB to ensure JPEG compatibility
+            if img.mode != 'RGB':
+                img = img.convert('RGB')
+                
+            # Resize if too large
+            max_dim = 2000
+            if max(img.width, img.height) > max_dim:
+                ratio = max_dim / max(img.width, img.height)
+                new_size = (int(img.width * ratio), int(img.height * ratio))
+                img = img.resize(new_size, Image.Resampling.LANCZOS)
+                
+            # Save as JPEG in memory
+            buffer = io.BytesIO()
+            img.save(buffer, format='JPEG', quality=85)
+            
+            b64_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
+            return f"data:image/jpeg;base64,{b64_str}"
+    except Exception as e:
+        print(f"Error normalizing image from {image_url}: {e}")
+        return image_url # Fallback to original URL if processing fails
+
 def process_ai_task(record_id, image_url):
     """Background task to process image with AI and store result."""
     print(f"[AI Task] Starting task for record {record_id}...")
@@ -134,6 +193,8 @@ def process_ai_task(record_id, image_url):
         Do not output any reasoning or explanation, just the JSON.
         """
 
+        ai_payload_url = get_normalized_base64_image(image_url)
+        
         payload = {
             "model": "doubao-seed-1-8-251228",
             "stream": True,  # Streaming for robust handling
@@ -141,7 +202,7 @@ def process_ai_task(record_id, image_url):
                 {
                     "role": "user",
                     "content": [
-                        {"type": "input_image", "image_url": image_url},
+                        {"type": "input_image", "image_url": ai_payload_url},
                         {"type": "input_text", "text": prompt}
                     ]
                 }
@@ -357,17 +418,51 @@ def process_ai_task(record_id, image_url):
 def index():
     if 'user_id' not in session:
         return redirect(url_for('login'))
+        
+    page = request.args.get('page', 1, type=int)
+    version = request.args.get('version', '').strip()
+    source = request.args.get('source', '').strip()
+    person = request.args.get('person', '').strip()
+    file_type = request.args.get('file_type', '').strip()
+    per_page = 10
+    offset = (page - 1) * per_page
     
     conn = get_db_connection()
     try:
         with conn.cursor() as cursor:
-            # 获取家谱图片记录 (上传管理)
-            cursor.execute("SELECT * FROM genealogy_records ORDER BY upload_time DESC")
+            query_conditions = []
+            params = []
+            if version:
+                query_conditions.append("genealogy_version LIKE %s")
+                params.append(f"%{version}%")
+            if source:
+                query_conditions.append("genealogy_source LIKE %s")
+                params.append(f"%{source}%")
+            if person:
+                query_conditions.append("upload_person LIKE %s")
+                params.append(f"%{person}%")
+            if file_type:
+                query_conditions.append("file_type = %s")
+                params.append(file_type)
+                
+            where_clause = ""
+            if query_conditions:
+                where_clause = "WHERE " + " AND ".join(query_conditions)
+                
+            count_sql = f"SELECT COUNT(*) as count FROM genealogy_records {where_clause}"
+            cursor.execute(count_sql, params)
+            total = cursor.fetchone()['count']
+            
+            sql = f"SELECT * FROM genealogy_records {where_clause} ORDER BY page_number ASC LIMIT %s OFFSET %s"
+            cursor.execute(sql, params + [per_page, offset])
             records = cursor.fetchall()
+            
+            total_pages = (total + per_page - 1) // per_page
+            
     finally:
         conn.close()
     
-    return render_template('index.html', records=records)
+    return render_template('index.html', records=records, page=page, total_pages=total_pages, version=version, source=source, person=person, file_type=file_type, total=total)
 
 @app.route('/manager/members')
 def members():
@@ -1051,6 +1146,8 @@ def recognize_image():
     如果包含多个人物,请都提取出来。
     """
 
+    ai_payload_url = get_normalized_base64_image(image_url)
+    
     payload = {
         "model": "doubao-seed-1-8-251228",
         "stream": True,
@@ -1060,7 +1157,7 @@ def recognize_image():
                 "content": [
                     {
                         "type": "input_image",
-                        "image_url": image_url
+                        "image_url": ai_payload_url
                     },
                     {
                         "type": "input_text",
@@ -1185,6 +1282,201 @@ def start_analysis(record_id):
     finally:
         conn.close()
 
+def process_files_background(saved_files, manual_page, suggested_page, genealogy_version, genealogy_source, upload_person):
+    current_suggested_page = int(manual_page) if manual_page and manual_page.isdigit() else suggested_page
+    
+    for filename, file_path in saved_files:
+        try:
+            if filename.lower().endswith('.pdf'):
+                doc = fitz.open(file_path)
+                for page_index in range(len(doc)):
+                    img_path = None
+                    try:
+                        page = doc.load_page(page_index)
+                        max_dim = max(page.rect.width, page.rect.height)
+                        zoom = 2000 / max_dim if max_dim > 0 else 2.0
+                        if zoom > 2.5: zoom = 2.5
+                        mat = fitz.Matrix(zoom, zoom)
+                        
+                        pix = page.get_pixmap(matrix=mat)
+                        img_filename = f"{os.path.splitext(filename)[0]}_page_{page_index+1}.jpg"
+                        img_path = os.path.join(app.config['UPLOAD_FOLDER'], img_filename)
+                        pix.save(img_path)
+                        
+                        oss_url = upload_to_oss(img_path)
+                        if oss_url:
+                            final_page = current_suggested_page
+                            conn = get_db_connection()
+                            try:
+                                with conn.cursor() as cursor:
+                                    sql = """INSERT INTO genealogy_records 
+                                             (file_name, oss_url, page_number, ai_status, genealogy_version, genealogy_source, upload_person, file_type) 
+                                             VALUES (%s, %s, %s, 1, %s, %s, %s, %s)"""
+                                    cursor.execute(sql, (img_filename, oss_url, final_page, genealogy_version, genealogy_source, upload_person, 'PDF'))
+                                    record_id = cursor.lastrowid
+                                conn.commit()
+                                threading.Thread(target=process_ai_task, args=(record_id, oss_url)).start()
+                                current_suggested_page += 1
+                            finally:
+                                conn.close()
+                    finally:
+                        if img_path and os.path.exists(img_path):
+                            try:
+                                os.remove(img_path)
+                            except:
+                                pass
+                doc.close()
+            else:
+                img_path = compress_image_if_needed(file_path)
+                
+                page_num = extract_page_number(img_path)
+                final_page = page_num if page_num else current_suggested_page
+                
+                ext = os.path.splitext(img_path)[1]
+                if genealogy_version and genealogy_source:
+                    if final_page is not None and str(final_page).strip() != '':
+                        img_filename = f"{genealogy_version}_{genealogy_source}_{final_page}{ext}"
+                    else:
+                        img_filename = f"{genealogy_version}_{genealogy_source}{ext}"
+                else:
+                    img_filename = os.path.basename(img_path)
+                
+                oss_url = upload_to_oss(img_path, custom_filename=img_filename)
+                if oss_url:
+                    conn = get_db_connection()
+                    try:
+                        with conn.cursor() as cursor:
+                            sql = """INSERT INTO genealogy_records 
+                                     (file_name, oss_url, page_number, ai_status, genealogy_version, genealogy_source, upload_person, file_type) 
+                                     VALUES (%s, %s, %s, 1, %s, %s, %s, %s)"""
+                            cursor.execute(sql, (img_filename, oss_url, final_page, genealogy_version, genealogy_source, upload_person, '图片'))
+                            record_id = cursor.lastrowid
+                        conn.commit()
+                        threading.Thread(target=process_ai_task, args=(record_id, oss_url)).start()
+                        if page_num:
+                            current_suggested_page = page_num + 1
+                        else:
+                            current_suggested_page += 1
+                    finally:
+                        conn.close()
+                if img_path != file_path and os.path.exists(img_path):
+                    try:
+                        os.remove(img_path)
+                    except:
+                        pass
+        except Exception as e:
+            print(f"Error processing file {filename}: {e}")
+        finally:
+            if os.path.exists(file_path):
+                try:
+                    os.remove(file_path)
+                except:
+                    pass
+
+def process_files_background(upload_folder, saved_files, manual_page, suggested_page, genealogy_version, genealogy_source, upload_person):
+    current_suggested_page = int(manual_page) if manual_page and str(manual_page).isdigit() else suggested_page
+    
+    for filename, file_path in saved_files:
+        try:
+            if filename.lower().endswith('.pdf'):
+                doc = fitz.open(file_path)
+                for page_index in range(len(doc)):
+                    img_path = None
+                    try:
+                        page = doc.load_page(page_index)
+                        max_dim = max(page.rect.width, page.rect.height)
+                        zoom = 2000 / max_dim if max_dim > 0 else 2.0
+                        if zoom > 2.5: zoom = 2.5
+                        mat = fitz.Matrix(zoom, zoom)
+                        
+                        # Use get_pixmap with matrix directly
+                        pix = page.get_pixmap(matrix=mat)
+                        
+                        final_page = current_suggested_page
+                        if genealogy_version and genealogy_source:
+                            if final_page is not None and str(final_page).strip() != '':
+                                img_filename = f"{genealogy_version}_{genealogy_source}_{final_page}.jpg"
+                            else:
+                                img_filename = f"{genealogy_version}_{genealogy_source}.jpg"
+                        else:
+                            img_filename = f"{os.path.splitext(filename)[0]}_page_{page_index+1}.jpg"
+                            
+                        img_path = os.path.join(upload_folder, img_filename)
+                        
+                        # Save the pixmap to the image path
+                        pix.save(img_path)
+                        
+                        oss_url = upload_to_oss(img_path, custom_filename=img_filename)
+                        if oss_url:
+                            conn = get_db_connection()
+                            try:
+                                with conn.cursor() as cursor:
+                                    sql = """INSERT INTO genealogy_records 
+                                             (file_name, oss_url, page_number, ai_status, genealogy_version, genealogy_source, upload_person, file_type) 
+                                             VALUES (%s, %s, %s, 1, %s, %s, %s, %s)"""
+                                    cursor.execute(sql, (img_filename, oss_url, final_page, genealogy_version, genealogy_source, upload_person, 'PDF'))
+                                    record_id = cursor.lastrowid
+                                conn.commit()
+                                threading.Thread(target=process_ai_task, args=(record_id, oss_url)).start()
+                                current_suggested_page += 1
+                            finally:
+                                conn.close()
+                    except Exception as page_e:
+                        print(f"Error processing page {page_index} of {filename}: {page_e}")
+                    finally:
+                        if img_path and os.path.exists(img_path):
+                            try:
+                                os.remove(img_path)
+                            except:
+                                pass
+                doc.close()
+            else:
+                img_path = compress_image_if_needed(file_path)
+                
+                page_num = extract_page_number(img_path)
+                final_page = page_num if page_num else current_suggested_page
+                
+                ext = os.path.splitext(img_path)[1]
+                if genealogy_version and genealogy_source:
+                    if final_page is not None and str(final_page).strip() != '':
+                        img_filename = f"{genealogy_version}_{genealogy_source}_{final_page}{ext}"
+                    else:
+                        img_filename = f"{genealogy_version}_{genealogy_source}{ext}"
+                else:
+                    img_filename = os.path.basename(img_path)
+                
+                oss_url = upload_to_oss(img_path, custom_filename=img_filename)
+                if oss_url:
+                    conn = get_db_connection()
+                    try:
+                        with conn.cursor() as cursor:
+                            sql = """INSERT INTO genealogy_records 
+                                     (file_name, oss_url, page_number, ai_status, genealogy_version, genealogy_source, upload_person, file_type) 
+                                     VALUES (%s, %s, %s, 1, %s, %s, %s, %s)"""
+                            cursor.execute(sql, (img_filename, oss_url, final_page, genealogy_version, genealogy_source, upload_person, '图片'))
+                            record_id = cursor.lastrowid
+                        conn.commit()
+                        threading.Thread(target=process_ai_task, args=(record_id, oss_url)).start()
+                        if page_num:
+                            current_suggested_page = page_num + 1
+                        else:
+                            current_suggested_page += 1
+                    finally:
+                        conn.close()
+                if img_path and img_path != file_path and os.path.exists(img_path):
+                    try:
+                        os.remove(img_path)
+                    except:
+                        pass
+        except Exception as e:
+            print(f"Error processing file {filename}: {e}")
+        finally:
+            if os.path.exists(file_path):
+                try:
+                    os.remove(file_path)
+                except:
+                    pass
+
 @app.route('/manager/upload', methods=['GET', 'POST'])
 def upload():
     if 'user_id' not in session:
@@ -1207,60 +1499,51 @@ def upload():
             flash('未选择文件')
             return redirect(request.url)
         
-        file = request.files['file']
-        if file.filename == '':
+        files = request.files.getlist('file')
+        if not files or files[0].filename == '':
             flash('未选择文件')
             return redirect(request.url)
             
-        # 允许用户在上传时直接指定页码,或者由 OCR 识别
         manual_page = request.form.get('manual_page')
-        
-        if file:
-            filename = secure_filename(file.filename)
-            file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
-            file.save(file_path)
+        genealogy_version = request.form.get('genealogy_version', '')
+        genealogy_source = request.form.get('genealogy_source', '')
+        upload_person = request.form.get('upload_person', '')
+        if not upload_person:
+            upload_person = session.get('username', '')
             
-            # 1. 尝试 OCR 提取页码
-            page_num = extract_page_number(file_path)
+        import uuid
+        saved_files = []
+        for file in files:
+            if not file or not file.filename:
+                continue
             
-            # 如果 OCR 没识别到,且用户也没手动输入,则跳转到“补录页码”页面
-            if not page_num and not manual_page:
-                # 先把文件传到 OSS,拿到 URL,方便后续补录
-                oss_url = upload_to_oss(file_path)
-                if oss_url:
-                    # 暂时存入 session 或者跳转带参数
-                    return render_template('confirm_page.html', 
-                                         filename=filename, 
-                                         oss_url=oss_url, 
-                                         suggested_page=suggested_page)
-                else:
-                    flash('上传 OSS 失败')
-                    return redirect(url_for('upload'))
-
-            # 使用识别到的或手动输入的页码
-            final_page = manual_page if manual_page else page_num
-            oss_url = upload_to_oss(file_path)
+            original_filename = file.filename
+            ext = os.path.splitext(original_filename)[1].lower()
+            base_name = secure_filename(original_filename)
             
-            if oss_url:
-                conn = get_db_connection()
-                try:
-                    with conn.cursor() as cursor:
-                        sql = "INSERT INTO genealogy_records (file_name, oss_url, page_number, ai_status) VALUES (%s, %s, %s, 1)"
-                        cursor.execute(sql, (filename, oss_url, final_page))
-                        record_id = cursor.lastrowid
-                    conn.commit()
-                    
-                    # Start AI Task
-                    threading.Thread(target=process_ai_task, args=(record_id, oss_url)).start()
-                    
-                    flash(f'上传成功!页码:{final_page},已加入解析队列')
-                except Exception as e:
-                    flash(f'数据库错误:{e}')
-                finally:
-                    conn.close()
-                return redirect(url_for('index'))
+            # If secure_filename removes all characters (e.g., pure Chinese name) or just leaves 'pdf'
+            if not base_name or base_name == ext.strip('.'):
+                filename = f"upload_{uuid.uuid4().hex[:8]}{ext}"
             else:
-                flash('上传 OSS 失败')
+                # Ensure the extension is preserved
+                if not base_name.lower().endswith(ext):
+                    filename = f"{base_name}{ext}"
+                else:
+                    filename = base_name
+                    
+            file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
+            file.save(file_path)
+            saved_files.append((filename, file_path))
+            
+            if saved_files:
+                threading.Thread(
+                    target=process_files_background,
+                    args=(app.config['UPLOAD_FOLDER'], saved_files, manual_page, suggested_page, genealogy_version, genealogy_source, upload_person)
+                ).start()
+                flash('上传完成,AI解析中,稍后查看')
+                
+            time.sleep(1.5)
+            return redirect(url_for('index'))
                 
     return render_template('upload.html', suggested_page=suggested_page)
 
@@ -1271,6 +1554,10 @@ def save_upload():
     filename = request.form.get('filename')
     oss_url = request.form.get('oss_url')
     page_number = request.form.get('page_number')
+    genealogy_version = request.form.get('genealogy_version', '')
+    genealogy_source = request.form.get('genealogy_source', '')
+    upload_person = request.form.get('upload_person', session.get('username', ''))
+    file_type = request.form.get('file_type', '图片')
     
     if not oss_url or not page_number:
         flash('页码不能为空')
@@ -1279,15 +1566,17 @@ def save_upload():
     conn = get_db_connection()
     try:
         with conn.cursor() as cursor:
-            sql = "INSERT INTO genealogy_records (file_name, oss_url, page_number, ai_status) VALUES (%s, %s, %s, 1)"
-            cursor.execute(sql, (filename, oss_url, page_number))
+            sql = """INSERT INTO genealogy_records 
+                     (file_name, oss_url, page_number, ai_status, genealogy_version, genealogy_source, upload_person, file_type) 
+                     VALUES (%s, %s, %s, 1, %s, %s, %s, %s)"""
+            cursor.execute(sql, (filename, oss_url, page_number, genealogy_version, genealogy_source, upload_person, file_type))
             record_id = cursor.lastrowid
         conn.commit()
         
         # Start AI Task
         threading.Thread(target=process_ai_task, args=(record_id, oss_url)).start()
         
-        flash(f'记录已保存,页码:{page_number},已加入解析队列')
+        flash('上传完成,AI解析中,稍后查看')
     except Exception as e:
         flash(f'保存失败: {e}')
     finally:

+ 32 - 0
db_update.py

@@ -0,0 +1,32 @@
+import pymysql
+
+DB_CONFIG = {
+    "host": "rm-f8ze60yirdj8786u2wo.mysql.rds.aliyuncs.com",
+    "port": 3306,
+    "user": "csqz",
+    "password": "csqz@2026",
+    "db": "csqz-client",
+    "charset": "utf8mb4",
+    "cursorclass": pymysql.cursors.DictCursor
+}
+
+def update_db():
+    conn = pymysql.connect(**DB_CONFIG)
+    try:
+        with conn.cursor() as cursor:
+            # Check if file_type column exists
+            cursor.execute("SHOW COLUMNS FROM genealogy_records LIKE 'file_type'")
+            if not cursor.fetchone():
+                cursor.execute("ALTER TABLE genealogy_records ADD COLUMN file_type VARCHAR(50) DEFAULT '图片'")
+                print("Added file_type")
+            else:
+                print("file_type already exists")
+        conn.commit()
+    except Exception as e:
+        print(f"Error: {e}")
+    finally:
+        conn.close()
+
+if __name__ == '__main__':
+    update_db()
+


+ 9 - 4
oss_utils.py

@@ -1,6 +1,8 @@
+import os
 import requests
+import mimetypes
 
-def upload_to_oss(file_path):
+def upload_to_oss(file_path, custom_filename=None):
     """
     Uploads a file to OSS using the provided API endpoint.
     URL: https://crmapi.dcjxb.yunzhixue.cn/file/upload
@@ -9,14 +11,17 @@ def upload_to_oss(file_path):
     """
     url = "https://crmapi.dcjxb.yunzhixue.cn/file/upload"
     try:
+        filename = custom_filename if custom_filename else os.path.basename(file_path)
+        mime_type, _ = mimetypes.guess_type(file_path)
+        if not mime_type:
+            mime_type = 'application/octet-stream'
+            
         with open(file_path, 'rb') as f:
-            files = {'file': f}
+            files = {'file': (filename, f, mime_type)}
             response = requests.post(url, files=files)
             response.raise_for_status()
             result = response.json()
             # Assuming the response contains the URL in a specific field
-            # We'll need to check the actual response format
-            # Typical format: {"code": 200, "data": {"url": "..."}} or similar
             if result.get('code') == 200 or result.get('success'):
                 # Extract URL - adjusting based on common API patterns
                 data = result.get('data', {})

+ 20 - 1
templates/add_member.html

@@ -321,6 +321,11 @@
                 当前: <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 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>
@@ -434,7 +439,10 @@
             url: "{{ img.oss_url }}", 
             page: {{ img.page_number or 0 }},
             ai_status: {{ img.ai_status or 0 }},
-            ai_content: {{ img.ai_content | tojson | safe if img.ai_content else 'null' }}
+            ai_content: {{ img.ai_content | tojson | safe if img.ai_content else 'null' }},
+            genealogy_version: "{{ img.genealogy_version or '' }}",
+            genealogy_source: "{{ img.genealogy_source or '' }}",
+            upload_person: "{{ img.upload_person or '' }}"
         },
         {% endfor %}
     ];
@@ -830,6 +838,17 @@
             document.getElementById('refImage').src = img.url;
             document.getElementById('currentPage').innerText = currentIndex + 1;
             
+            // Update metadata display
+            const metaContainer = document.getElementById('imageMetadata');
+            if (img.genealogy_version || img.genealogy_source || img.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 || '未提供';
+            } else {
+                metaContainer.style.display = 'none';
+            }
+            
             // Reset image state on switch
             resetFilters();
             

+ 4 - 0
templates/confirm_page.html

@@ -24,6 +24,10 @@
                         <form action="{{ url_for('save_upload') }}" method="POST">
                             <input type="hidden" name="filename" value="{{ filename }}">
                             <input type="hidden" name="oss_url" value="{{ oss_url }}">
+                            <input type="hidden" name="genealogy_version" value="{{ genealogy_version }}">
+                            <input type="hidden" name="genealogy_source" value="{{ genealogy_source }}">
+                            <input type="hidden" name="upload_person" value="{{ upload_person }}">
+                            <input type="hidden" name="file_type" value="{{ file_type }}">
                             
                             <div class="mb-3">
                                 <label class="form-label fw-bold">页码</label>

+ 83 - 1
templates/index.html

@@ -92,6 +92,37 @@
     </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">
@@ -100,6 +131,7 @@
                     <tr>
                         <th class="px-4">文件名</th>
                         <th>页码</th>
+                        <th>提供人</th>
                         <th>AI 解析状态</th>
                         <th>上传时间</th>
                         <th class="text-center">操作</th>
@@ -111,7 +143,14 @@
                         <td class="px-4">
                             <div class="d-flex align-items-center">
                                 <img src="{{ record.oss_url }}" class="thumbnail-img me-3" onclick="openImageViewer('{{ record.oss_url }}', {{ record.page_number }})" title="点击预览">
-                                <div class="text-break">{{ record.file_name }}</div>
+                                <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>
@@ -121,6 +160,19 @@
                                 <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>
@@ -174,6 +226,36 @@
             </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 -->

+ 5 - 0
templates/member_detail.html

@@ -256,6 +256,11 @@
                         <i class="bi bi-arrows-fullscreen"></i> 点击查看大图 (第{{ member.source_page }}页)
                     </div>
                 </div>
+                <div class="mt-3 small text-muted bg-light p-2 rounded">
+                    <div class="mb-1"><i class="bi bi-journal-text me-1"></i><strong>版本名称:</strong> {{ member.genealogy_version or '未提供' }}</div>
+                    <div class="mb-1"><i class="bi bi-archive me-1"></i><strong>版本来源:</strong> {{ member.genealogy_source or '未提供' }}</div>
+                    <div><i class="bi bi-person me-1"></i><strong>文件提供人:</strong> {{ member.upload_person or '未提供' }}</div>
+                </div>
             </div>
             {% endif %}
 

+ 63 - 4
templates/upload.html

@@ -10,12 +10,27 @@
                 <h5 class="mb-0"><i class="bi bi-cloud-upload me-2"></i>上传家谱扫描件</h5>
             </div>
             <div class="card-body p-4">
-                <form method="POST" enctype="multipart/form-data">
+                <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" class="form-control form-control-lg" required>
+                        <input type="file" name="file" id="fileInput" class="form-control form-control-lg" multiple accept=".jpg,.jpeg,.png,.pdf" required>
                         <div class="form-text mt-2">
-                            支持图片 (JPG, PNG) 或 PDF 格式的扫描件。上传后将自动识别页码。
+                            支持图片 (JPG, PNG) 或 PDF 格式的扫描件。图片支持多选(一次最多10张)。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="genealogy_version" 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="genealogy_source" class="form-control" placeholder="如:留越收藏" required>
+                        </div>
+                        <div class="col-md-4">
+                            <label class="form-label fw-bold">文件提供人</label>
+                            <input type="text" name="upload_person" class="form-control" placeholder="默认:当前系统登录账号">
                         </div>
                     </div>
 
@@ -39,7 +54,7 @@
 
                     <div class="d-grid gap-2 d-md-flex justify-content-md-end">
                         <a href="{{ url_for('index') }}" class="btn btn-light px-4">取消</a>
-                        <button type="submit" class="btn btn-primary px-5">
+                        <button type="submit" class="btn btn-primary px-5" id="submitBtn">
                             <i class="bi bi-check-lg me-1"></i> 开始上传
                         </button>
                     </div>
@@ -49,3 +64,47 @@
     </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;
+        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 {
+                imgCount++;
+            }
+        }
+
+        if (imgCount > 10) {
+            alert('一次最多只能上传10张图片,请重新选择。');
+            e.target.value = ''; // clear selection
+        } else if (pdfCount > 0 && files.length > 1) {
+            alert('上传PDF时,一次只能选择1个文件。');
+            e.target.value = ''; // clear selection
+        }
+    });
+</script>
+{% endblock %}



BIN
uploads/20260302110449_225_93.jpg