PUGE vor 3 Wochen
Ursprung
Commit
e27f166a07
3 geänderte Dateien mit 1017 neuen und 0 gelöschten Zeilen
  1. 567 0
      templates/index.html
  2. 0 0
      templates_excel/模板.xlsx
  3. 450 0
      web.py

+ 567 - 0
templates/index.html

@@ -0,0 +1,567 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Excel 自动填表工具</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+            background: #f5f7fa;
+            color: #333;
+            padding: 20px;
+        }
+        .container {
+            max-width: 1100px;
+            margin: 0 auto;
+            background: #fff;
+            border-radius: 12px;
+            box-shadow: 0 2px 12px rgba(0,0,0,0.08);
+            padding: 30px;
+        }
+        h1 {
+            font-size: 24px;
+            color: #1a1a2e;
+            margin-bottom: 8px;
+            display: flex;
+            align-items: center;
+            gap: 10px;
+        }
+        .subtitle {
+            color: #888;
+            font-size: 14px;
+            margin-bottom: 24px;
+            border-bottom: 1px solid #eee;
+            padding-bottom: 16px;
+        }
+        .form-group {
+            margin-bottom: 18px;
+        }
+        .form-group label {
+            display: block;
+            font-weight: 600;
+            font-size: 14px;
+            margin-bottom: 5px;
+            color: #444;
+        }
+        .form-group .hint {
+            font-size: 12px;
+            color: #999;
+            font-weight: 400;
+        }
+        .form-row {
+            display: flex;
+            gap: 20px;
+            flex-wrap: wrap;
+        }
+        .form-row .form-group {
+            flex: 1;
+            min-width: 180px;
+        }
+        select, input[type="file"], input[type="number"] {
+            width: 100%;
+            padding: 10px 12px;
+            border: 1px solid #d1d5db;
+            border-radius: 8px;
+            font-size: 14px;
+            background: #fafbfc;
+            transition: border-color 0.2s;
+        }
+        select:focus, input:focus {
+            border-color: #4f8cf7;
+            outline: none;
+            box-shadow: 0 0 0 3px rgba(79, 140, 247, 0.15);
+        }
+        .file-upload-wrapper {
+            position: relative;
+            display: flex;
+            align-items: center;
+            gap: 12px;
+        }
+        .file-upload-wrapper input[type="file"] {
+            padding: 8px;
+        }
+        .file-name {
+            font-size: 13px;
+            color: #666;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            max-width: 200px;
+        }
+        .btn {
+            padding: 10px 28px;
+            border: none;
+            border-radius: 8px;
+            font-size: 15px;
+            font-weight: 600;
+            cursor: pointer;
+            transition: all 0.2s;
+            display: inline-flex;
+            align-items: center;
+            gap: 8px;
+        }
+        .btn-primary {
+            background: #4f8cf7;
+            color: #fff;
+        }
+        .btn-primary:hover {
+            background: #3a7ae8;
+            transform: translateY(-1px);
+        }
+        .btn-primary:disabled {
+            background: #b0c8f5;
+            cursor: not-allowed;
+            transform: none;
+        }
+        .btn-success {
+            background: #34c759;
+            color: #fff;
+        }
+        .btn-success:hover {
+            background: #2db84e;
+        }
+        .btn-success:disabled {
+            background: #9de0b2;
+            cursor: not-allowed;
+        }
+        .btn-secondary {
+            background: #e8ecf1;
+            color: #444;
+        }
+        .btn-secondary:hover {
+            background: #d5dbe3;
+        }
+        .btn-danger {
+            background: #ef4444;
+            color: #fff;
+        }
+        .btn-danger:hover {
+            background: #dc2626;
+        }
+        .actions {
+            display: flex;
+            gap: 12px;
+            flex-wrap: wrap;
+            margin: 20px 0 16px;
+            padding: 16px 0;
+            border-top: 1px solid #eee;
+            border-bottom: 1px solid #eee;
+        }
+        .log-area {
+            background: #1a1a2e;
+            color: #e4e4e4;
+            border-radius: 8px;
+            padding: 16px 20px;
+            font-family: 'Courier New', monospace;
+            font-size: 12px;
+            max-height: 320px;
+            overflow-y: auto;
+            white-space: pre-wrap;
+            word-break: break-all;
+            line-height: 1.6;
+            margin-top: 16px;
+        }
+        .log-area .log-success {
+            color: #4ade80;
+        }
+        .log-area .log-error {
+            color: #f87171;
+        }
+        .log-area .log-warn {
+            color: #fbbf24;
+        }
+        .log-area .log-info {
+            color: #60a5fa;
+        }
+        .preview-table-wrapper {
+            overflow-x: auto;
+            margin-top: 16px;
+            border-radius: 8px;
+            border: 1px solid #e5e7eb;
+        }
+        .preview-table {
+            width: 100%;
+            border-collapse: collapse;
+            font-size: 13px;
+            min-width: 600px;
+        }
+        .preview-table th {
+            background: #f3f4f6;
+            padding: 10px 12px;
+            text-align: left;
+            font-weight: 600;
+            border-bottom: 2px solid #e5e7eb;
+            white-space: nowrap;
+            position: sticky;
+            top: 0;
+            z-index: 1;
+        }
+        .preview-table td {
+            padding: 8px 12px;
+            border-bottom: 1px solid #e5e7eb;
+            max-width: 200px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+        .preview-table tr:hover td {
+            background: #f9fafb;
+        }
+        .status-badge {
+            display: inline-block;
+            padding: 4px 14px;
+            border-radius: 20px;
+            font-size: 13px;
+            font-weight: 500;
+        }
+        .status-badge.idle {
+            background: #e5e7eb;
+            color: #6b7280;
+        }
+        .status-badge.loading {
+            background: #fef3c7;
+            color: #d97706;
+        }
+        .status-badge.success {
+            background: #d1fae5;
+            color: #065f46;
+        }
+        .status-badge.error {
+            background: #fee2e2;
+            color: #991b1b;
+        }
+        .flex {
+            display: flex;
+            align-items: center;
+            gap: 12px;
+        }
+        .flex-between {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            flex-wrap: wrap;
+            gap: 10px;
+        }
+        .mt-8 { margin-top: 8px; }
+        .mt-12 { margin-top: 12px; }
+        .text-center { text-align: center; }
+        .text-sm { font-size: 13px; }
+        .text-muted { color: #888; }
+        .hidden { display: none !important; }
+        
+        .download-section {
+            background: #ecfdf5;
+            border: 1px solid #6ee7b7;
+            border-radius: 8px;
+            padding: 16px 20px;
+            display: none;
+            align-items: center;
+            justify-content: space-between;
+            flex-wrap: wrap;
+            gap: 12px;
+            margin-top: 12px;
+        }
+        .download-section.show {
+            display: flex;
+        }
+    </style>
+</head>
+<body>
+<div class="container">
+    <h1>📊 Excel 自动填表工具</h1>
+    <div class="subtitle">
+        选择模板 → 上传来源数据 → 预览 → 生成并下载
+        <span class="status-badge idle" id="statusBadge">就绪</span>
+    </div>
+
+    <!-- 配置区域 -->
+    <div class="form-row">
+        <div class="form-group">
+            <label>📁 选择模板 <span class="hint">(模板文件需放在服务器 templates_excel/ 目录)</span></label>
+            <select id="templateSelect">
+                <option value="">-- 请选择模板 --</option>
+                {% for t in templates %}
+                <option value="{{ t }}">{{ t }}</option>
+                {% endfor %}
+            </select>
+        </div>
+        <div class="form-group">
+            <label>📄 来源数据 Excel</label>
+            <div class="file-upload-wrapper">
+                <input type="file" id="sourceFile" accept=".xlsx,.xls">
+                <span class="file-name" id="fileName">未选择文件</span>
+            </div>
+        </div>
+    </div>
+
+    <div class="form-row">
+        <div class="form-group">
+            <label>来源表头行号 <span class="hint">(第2行是字段名)</span></label>
+            <input type="number" id="sourceHeader" value="2" min="1">
+        </div>
+        <div class="form-group">
+            <label>模板表头行号 <span class="hint">(第4行是字段名)</span></label>
+            <input type="number" id="templateHeader" value="4" min="1">
+        </div>
+    </div>
+
+    <!-- 操作按钮 -->
+    <div class="actions">
+        <button class="btn btn-primary" id="previewBtn">🔍 预览数据</button>
+        <button class="btn btn-success" id="generateBtn" disabled>🚀 生成并下载</button>
+        <button class="btn btn-secondary" id="clearLogBtn">🗑️ 清空日志</button>
+    </div>
+
+    <!-- 状态信息 -->
+    <div class="flex-between">
+        <span id="statusText" class="text-muted text-sm">请选择模板和来源文件</span>
+        <span id="rowCount" class="text-muted text-sm"></span>
+    </div>
+
+    <!-- 预览表格 -->
+    <div id="previewContainer" class="hidden">
+        <div class="flex-between mt-8">
+            <span class="text-sm" style="font-weight:600;">📋 数据预览 (前10行)</span>
+        </div>
+        <div class="preview-table-wrapper" id="previewWrapper">
+            <table class="preview-table" id="previewTable">
+                <thead id="previewHead"></thead>
+                <tbody id="previewBody"></tbody>
+            </table>
+        </div>
+    </div>
+
+    <!-- 下载区域 -->
+    <div class="download-section" id="downloadSection">
+        <span style="font-weight:600;color:#065f46;">✅ 生成完成!</span>
+        <span id="downloadInfo"></span>
+        <a href="#" id="downloadLink" class="btn btn-success" style="text-decoration:none;">📥 下载文件</a>
+    </div>
+
+    <!-- 日志 -->
+    <div class="log-area" id="logArea">
+        <span class="log-info">💡 系统就绪,等待操作...</span>
+    </div>
+</div>
+
+<script>
+    // DOM 元素
+    const templateSelect = document.getElementById('templateSelect');
+    const sourceFile = document.getElementById('sourceFile');
+    const fileName = document.getElementById('fileName');
+    const sourceHeader = document.getElementById('sourceHeader');
+    const templateHeader = document.getElementById('templateHeader');
+    const previewBtn = document.getElementById('previewBtn');
+    const generateBtn = document.getElementById('generateBtn');
+    const clearLogBtn = document.getElementById('clearLogBtn');
+    const logArea = document.getElementById('logArea');
+    const statusText = document.getElementById('statusText');
+    const statusBadge = document.getElementById('statusBadge');
+    const rowCount = document.getElementById('rowCount');
+    const previewContainer = document.getElementById('previewContainer');
+    const previewHead = document.getElementById('previewHead');
+    const previewBody = document.getElementById('previewBody');
+    const downloadSection = document.getElementById('downloadSection');
+    const downloadLink = document.getElementById('downloadLink');
+    const downloadInfo = document.getElementById('downloadInfo');
+
+    let sourcePath = null;
+
+    // 日志函数
+    function log(msg, type = 'info') {
+        const colors = {
+            info: 'log-info',
+            success: 'log-success',
+            error: 'log-error',
+            warn: 'log-warn'
+        };
+        const prefix = {
+            info: 'ℹ️',
+            success: '✅',
+            error: '❌',
+            warn: '⚠️'
+        };
+        const timestamp = new Date().toLocaleTimeString();
+        const line = document.createElement('div');
+        line.innerHTML = `<span style="color:#888;">[${timestamp}]</span> <span class="${colors[type] || 'log-info'}">${prefix[type] || 'ℹ️'} ${msg}</span>`;
+        logArea.appendChild(line);
+        logArea.scrollTop = logArea.scrollHeight;
+    }
+
+    function setStatus(text, type = 'idle') {
+        statusText.textContent = text;
+        statusBadge.textContent = {
+            idle: '就绪',
+            loading: '处理中...',
+            success: '成功',
+            error: '失败'
+        }[type] || '就绪';
+        statusBadge.className = `status-badge ${type}`;
+    }
+
+    function clearLog() {
+        logArea.innerHTML = '';
+        log('日志已清空', 'info');
+    }
+
+    // 文件选择
+    sourceFile.addEventListener('change', function() {
+        if (this.files.length > 0) {
+            fileName.textContent = this.files[0].name;
+        } else {
+            fileName.textContent = '未选择文件';
+        }
+        checkReady();
+    });
+
+    templateSelect.addEventListener('change', checkReady);
+
+    function checkReady() {
+        const hasTemplate = templateSelect.value !== '';
+        const hasFile = sourceFile.files.length > 0;
+        previewBtn.disabled = !(hasTemplate && hasFile);
+        generateBtn.disabled = true;
+        if (hasTemplate && hasFile) {
+            setStatus('已选择模板和来源文件,点击预览', 'idle');
+        } else {
+            setStatus('请选择模板和来源文件', 'idle');
+        }
+    }
+
+    // 预览
+    previewBtn.addEventListener('click', async function() {
+        const template = templateSelect.value;
+        const file = sourceFile.files[0];
+        const sHeader = parseInt(sourceHeader.value) || 2;
+
+        if (!template || !file) {
+            log('请选择模板和来源文件', 'warn');
+            return;
+        }
+
+        this.disabled = true;
+        setStatus('正在预览...', 'loading');
+        log(`📖 预览来源数据: ${file.name}`, 'info');
+
+        const formData = new FormData();
+        formData.append('source_file', file);
+        formData.append('template_name', template);
+        formData.append('source_header', sHeader);
+
+        try {
+            const response = await fetch('/preview', {
+                method: 'POST',
+                body: formData
+            });
+            const result = await response.json();
+
+            if (result.error) {
+                log(`❌ 预览失败: ${result.error}`, 'error');
+                setStatus('预览失败', 'error');
+                previewContainer.classList.add('hidden');
+                return;
+            }
+
+            sourcePath = result.source_path;
+            log(`✅ 成功读取 ${result.total_rows} 行数据`, 'success');
+            setStatus(`预览成功,共 ${result.total_rows} 行`, 'success');
+            rowCount.textContent = `共 ${result.total_rows} 行`;
+
+            // 渲染表格
+            renderPreview(result.columns, result.data);
+            previewContainer.classList.remove('hidden');
+
+            // 启用生成按钮
+            generateBtn.disabled = false;
+
+        } catch (err) {
+            log(`❌ 请求失败: ${err.message}`, 'error');
+            setStatus('预览失败', 'error');
+        } finally {
+            this.disabled = false;
+        }
+    });
+
+    function renderPreview(columns, data) {
+        // 表头
+        previewHead.innerHTML = `<tr>${columns.map(c => `<th>${escapeHtml(c)}</th>`).join('')}</tr>`;
+        
+        // 数据行
+        previewBody.innerHTML = data.map(row => {
+            return `<tr>${columns.map(c => `<td title="${escapeHtml(String(row[c] || ''))}">${escapeHtml(String(row[c] || ''))}</td>`).join('')}</tr>`;
+        }).join('');
+    }
+
+    function escapeHtml(text) {
+        const div = document.createElement('div');
+        div.textContent = text;
+        return div.innerHTML;
+    }
+
+    // 生成
+    generateBtn.addEventListener('click', async function() {
+        if (!sourcePath) {
+            log('请先预览数据', 'warn');
+            return;
+        }
+
+        const template = templateSelect.value;
+        const sHeader = parseInt(sourceHeader.value) || 2;
+        const tHeader = parseInt(templateHeader.value) || 4;
+
+        this.disabled = true;
+        previewBtn.disabled = true;
+        setStatus('正在生成...', 'loading');
+        log('🚀 开始生成并追加到模板...', 'info');
+
+        const formData = new FormData();
+        formData.append('source_path', sourcePath);
+        formData.append('template_name', template);
+        formData.append('source_header', sHeader);
+        formData.append('header_row', tHeader);
+
+        try {
+            const response = await fetch('/generate', {
+                method: 'POST',
+                body: formData
+            });
+            const result = await response.json();
+
+            if (result.error) {
+                log(`❌ 生成失败: ${result.error}`, 'error');
+                setStatus('生成失败', 'error');
+                return;
+            }
+
+            log(`✅ ${result.message}`, 'success');
+            setStatus(`生成成功,追加 ${result.filled_count} 行`, 'success');
+
+            // 显示下载
+            downloadSection.classList.add('show');
+            downloadLink.href = result.download_url;
+            downloadInfo.textContent = `已追加 ${result.filled_count} 行数据`;
+
+        } catch (err) {
+            log(`❌ 请求失败: ${err.message}`, 'error');
+            setStatus('生成失败', 'error');
+        } finally {
+            this.disabled = false;
+            previewBtn.disabled = false;
+        }
+    });
+
+    // 清空日志
+    clearLogBtn.addEventListener('click', clearLog);
+
+    // 初始检查
+    checkReady();
+    log('💡 请选择模板和来源Excel文件,然后点击"预览数据"', 'info');
+</script>
+</body>
+</html>

+ 0 - 0
模板.xlsx → templates_excel/模板.xlsx


+ 450 - 0
web.py

@@ -0,0 +1,450 @@
+from flask import Flask, render_template, request, jsonify, send_file
+import pandas as pd
+from openpyxl import load_workbook
+import os
+import shutil
+from datetime import datetime
+import re
+from collections import Counter
+
+app = Flask(__name__)
+app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024  # 50MB
+
+# 配置路径
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
+TEMPLATES_FOLDER = os.path.join(BASE_DIR, 'templates_excel')
+OUTPUT_FOLDER = os.path.join(BASE_DIR, 'output')
+
+# 创建必要的目录
+for folder in [UPLOAD_FOLDER, TEMPLATES_FOLDER, OUTPUT_FOLDER]:
+    if not os.path.exists(folder):
+        os.makedirs(folder)
+
+
+def load_valid_values(wb):
+    """从 Valid Values 工作表读取有效值"""
+    valid_data = {}
+    
+    if "Valid Values" not in wb.sheetnames:
+        return valid_data
+    
+    ws_valid = wb["Valid Values"]
+    
+    # 读取第1行作为列名
+    headers = []
+    for col in range(1, ws_valid.max_column + 1):
+        val = ws_valid.cell(row=1, column=col).value
+        if val:
+            headers.append(str(val).strip())
+        else:
+            headers.append(f"Column_{col}")
+    
+    # 读取数据行
+    for row in range(2, ws_valid.max_row + 1):
+        for col in range(1, len(headers) + 1):
+            val = ws_valid.cell(row=row, column=col).value
+            if val:
+                header = headers[col - 1]
+                if header not in valid_data:
+                    valid_data[header] = []
+                if str(val).strip() not in valid_data[header]:
+                    valid_data[header].append(str(val).strip())
+    
+    return valid_data
+
+
+def parse_excel_file(file_path, source_header_row):
+    """读取来源Excel"""
+    df = pd.read_excel(file_path, header=source_header_row - 1)
+    return df
+
+
+def get_template_list():
+    """获取所有可用模板"""
+    templates = []
+    for f in os.listdir(TEMPLATES_FOLDER):
+        if f.endswith('.xlsx') or f.endswith('.xls'):
+            templates.append(f)
+    return templates
+
+
+@app.route('/')
+def index():
+    templates = get_template_list()
+    return render_template('index.html', templates=templates)
+
+
+@app.route('/get_templates')
+def get_templates():
+    return jsonify({'templates': get_template_list()})
+
+
+@app.route('/preview', methods=['POST'])
+def preview():
+    """预览来源数据"""
+    try:
+        source_file = request.files.get('source_file')
+        template_name = request.form.get('template_name')
+        source_header = int(request.form.get('source_header', 2))
+        
+        if not source_file:
+            return jsonify({'error': '请选择来源文件'}), 400
+        
+        if not template_name:
+            return jsonify({'error': '请选择模板'}), 400
+        
+        # 保存来源文件
+        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+        source_path = os.path.join(UPLOAD_FOLDER, f'source_{timestamp}_{source_file.filename}')
+        source_file.save(source_path)
+        
+        # 读取来源数据
+        df = pd.read_excel(source_path, header=source_header - 1)
+        
+        # 预览前10行
+        preview_data = df.head(10).fillna('').to_dict('records')
+        columns = df.columns.tolist()
+        
+        return jsonify({
+            'success': True,
+            'columns': columns,
+            'data': preview_data,
+            'total_rows': len(df),
+            'source_path': source_path
+        })
+        
+    except Exception as e:
+        return jsonify({'error': str(e)}), 500
+
+
+@app.route('/generate', methods=['POST'])
+def generate():
+    """生成并下载"""
+    try:
+        source_path = request.form.get('source_path')
+        template_name = request.form.get('template_name')
+        source_header = int(request.form.get('source_header', 2))
+        header_row = int(request.form.get('header_row', 4))
+        
+        if not source_path or not os.path.exists(source_path):
+            return jsonify({'error': '来源文件不存在'}), 400
+        
+        if not template_name:
+            return jsonify({'error': '请选择模板'}), 400
+        
+        template_path = os.path.join(TEMPLATES_FOLDER, template_name)
+        if not os.path.exists(template_path):
+            return jsonify({'error': '模板文件不存在'}), 400
+        
+        # ========== 读取来源数据 ==========
+        df = pd.read_excel(source_path, header=source_header - 1)
+        
+        # ========== 加载模板 ==========
+        wb = load_workbook(template_path)
+        ws = wb.active
+        
+        # 加载 Valid Values
+        valid_values = load_valid_values(wb)
+        
+        # ========== 读取模板表头 ==========
+        col_indices = {}
+        for col in range(1, ws.max_column + 1):
+            cell_value = ws.cell(row=header_row, column=col).value
+            if cell_value:
+                col_name = str(cell_value).strip()
+                if col_name and not col_name.startswith("Unnamed"):
+                    col_indices[col_name] = col
+        
+        # ========== 找到已有数据的最后一行 ==========
+        last_row = 5
+        for row in range(5, ws.max_row + 2):
+            is_empty = True
+            for col in range(1, min(20, ws.max_column + 1)):
+                if ws.cell(row=row, column=col).value is not None:
+                    is_empty = False
+                    break
+            if is_empty:
+                last_row = row
+                break
+        else:
+            last_row = ws.max_row + 1
+        
+        # ========== 匹配来源列 ==========
+        source_mapping = {
+            "Item Code": "Item Code",
+            "产品名称": "产品名称",
+            "优惠单价": "优惠单价",
+            "产品重量": "产品重量(磅)",
+            "包装尺寸-重量": "包装尺寸-重量(磅)",
+            "包装尺寸-高度": "包装尺寸-高度(英寸)",
+            "包装尺寸-宽度": "包装尺寸-宽度(英寸)",
+            "包装尺寸-长度": "包装尺寸-长度(英寸)",
+            "颜色": "颜色",
+            "材质": "材质",
+            "组装长度": "组装长度(英寸)",
+            "组装高度": "组装高度(英寸)",
+            "组装宽度": "组装宽度(英寸)",
+            "产品类目": "产品类目",
+        }
+        
+        source_map = {}
+        for key, expected in source_mapping.items():
+            found = False
+            for col in df.columns:
+                if col == expected:
+                    source_map[key] = col
+                    found = True
+                    break
+            if not found:
+                source_map[key] = None
+        
+        # ========== 默认值映射 ==========
+        default_mapping = {
+            "Variant Type": ("fixed", "Not Variant"),
+            "Minimum Order Quantity": ("fixed", 1),
+            "Force Quantity Multiplier": ("fixed", 1),
+            "Display Set Quantity": ("fixed", 1),
+            "Ship Type": ("valid", "Ship Type"),
+            "Lead Time": ("fixed", 48),
+            "Replacement Lead Time": ("fixed", 120),
+            "Flat Pack": ("fixed", "Yes"),
+            "Assembly Required": ("fixed", "Yes"),
+            "Canada Product Restriction": ("fixed", "Yes"),
+            "CARB Phase II Compliant (formaldehyde emissions)": ("fixed", "Yes"),
+            "Commercial Warranty": ("fixed", "Yes"),
+            "CANFER Compliant": ("fixed", "Yes"),
+            "Commercial Warranty Length": ("fixed", "1 Years"),
+            "Composite Wood Product (CWP)": ("fixed", "Yes"),
+            "Country Of Manufacturer": ("fixed", "China"),
+            "General Certificate of Conformity (GCC)": ("fixed", "Yes"),
+            "Hazard Class(es)": ("fixed", "Does Not Apply"),
+            "Hazardous Material / Dangerous Goods": ("fixed", "No"),
+            "Hazardous Material Weight": ("fixed", "Does Not Apply"),
+            "Level of Assembly": ("fixed", "Full Assembly Needed"),
+            "Packing Group": ("fixed", "Does Not Apply"),
+            "Reason for Restriction": ("fixed", "Does Not Apply"),
+            "Supplier Intended and Approved Use": ("fixed", "Residential Use"),
+            "TSCA Title VI Compliant (formaldehyde emissions)": ("fixed", "Yes"),
+            "UN or ID number": ("fixed", "Does Not Apply"),
+            "Uniform Packaging and Labeling Regulations (UPLR) Compliant": ("fixed", "Yes"),
+            "Warning Required": ("fixed", "No"),
+            "Warranty Length": ("fixed", "1 Years"),
+            "Battery or Batteries Included": ("fixed", "No"),
+            "Product Type": ("source", "产品类目"),
+            "Brand": ("valid", "Brand"),
+        }
+        
+        # ========== 填充数据 ==========
+        def get_key(row):
+            item = row.get("Item Code", "")
+            name = row.get("产品名称", "")
+            return f"{item}_{name}"
+        
+        keys = [get_key(row) for _, row in df.iterrows()]
+        key_count = Counter(keys)
+        key_index = Counter()
+        
+        row_num = last_row
+        filled_count = 0
+        
+        for idx, (_, src_row) in enumerate(df.iterrows()):
+            item_code = src_row.get("Item Code", "")
+            product_name = src_row.get("产品名称", "")
+            
+            if pd.isna(item_code):
+                item_code = ""
+            if pd.isna(product_name):
+                product_name = ""
+            
+            key = get_key(src_row)
+            
+            if key_count[key] > 1:
+                key_index[key] += 1
+                suffix = f".{key_index[key] - 1}"
+                mfr_suffix = f".{key_index[key]}"
+            else:
+                suffix = ""
+                mfr_suffix = ""
+            
+            fill_count = 0
+            
+            # Supplier Part Number
+            if "Supplier Part Number" in col_indices:
+                val = f"{item_code}{product_name}{suffix}"
+                ws.cell(row=row_num, column=col_indices["Supplier Part Number"], value=val)
+                fill_count += 1
+            
+            # Manufacturer Part Number
+            if "Manufacturer Part Number" in col_indices:
+                val = f"{item_code}{product_name}{mfr_suffix}"
+                ws.cell(row=row_num, column=col_indices["Manufacturer Part Number"], value=val)
+                fill_count += 1
+            
+            # Base Cost
+            if "Base Cost" in col_indices and source_map.get("优惠单价"):
+                col = source_map["优惠单价"]
+                val = src_row.get(col, "")
+                if pd.notna(val):
+                    ws.cell(row=row_num, column=col_indices["Base Cost"], value=val)
+                    fill_count += 1
+            
+            # MSRP
+            if "Manufacturer Suggested Retail Price" in col_indices and source_map.get("优惠单价"):
+                col = source_map["优惠单价"]
+                val = src_row.get(col, 0)
+                try:
+                    num = float(val) if pd.notna(val) else 0
+                    ws.cell(row=row_num, column=col_indices["Manufacturer Suggested Retail Price"], value=num + 70)
+                    fill_count += 1
+                except:
+                    pass
+            
+            # Product Weight
+            if "Product Weight" in col_indices and source_map.get("产品重量"):
+                col = source_map["产品重量"]
+                val = src_row.get(col, "")
+                if pd.notna(val):
+                    ws.cell(row=row_num, column=col_indices["Product Weight"], value=val)
+                    fill_count += 1
+            
+            # Carton
+            carton_map = [
+                ("Carton Weight 1", "包装尺寸-重量"),
+                ("Carton Height 1", "包装尺寸-高度"),
+                ("Carton Width 1", "包装尺寸-宽度"),
+                ("Carton Depth 1", "包装尺寸-长度"),
+            ]
+            for target, src_name in carton_map:
+                if target in col_indices and source_map.get(src_name):
+                    col = source_map[src_name]
+                    val = src_row.get(col, "")
+                    if pd.notna(val):
+                        ws.cell(row=row_num, column=col_indices[target], value=val)
+                        fill_count += 1
+            
+            # Color/Material
+            cm_map = [
+                ("Base Color", "颜色"),
+                ("Base Material", "材质"),
+                ("Color", "颜色"),
+                ("Material", "材质"),
+                ("Top Color", "颜色"),
+                ("Top Material", "材质"),
+            ]
+            for target, src_name in cm_map:
+                if target in col_indices and source_map.get(src_name):
+                    col = source_map[src_name]
+                    val = src_row.get(col, "")
+                    if pd.notna(val):
+                        ws.cell(row=row_num, column=col_indices[target], value=val)
+                        fill_count += 1
+            
+            # Overall
+            overall_map = [
+                ("Overall Depth - Front to Back", "组装长度"),
+                ("Overall Height - Top to Bottom", "组装高度"),
+                ("Overall Product Weight", "产品重量"),
+                ("Overall Width - Side to Side", "组装宽度"),
+            ]
+            for target, src_name in overall_map:
+                if target in col_indices and source_map.get(src_name):
+                    col = source_map[src_name]
+                    val = src_row.get(col, "")
+                    if pd.notna(val):
+                        ws.cell(row=row_num, column=col_indices[target], value=val)
+                        fill_count += 1
+            
+            # 默认值字段
+            variant_type = "Not Variant"
+            
+            for target_col, (source_type, source_val) in default_mapping.items():
+                if target_col not in col_indices:
+                    continue
+                
+                col_idx = col_indices[target_col]
+                
+                if source_type == "fixed":
+                    ws.cell(row=row_num, column=col_idx, value=source_val)
+                    fill_count += 1
+                
+                elif source_type == "valid":
+                    if source_val in valid_values and valid_values[source_val]:
+                        val = valid_values[source_val][0]
+                        ws.cell(row=row_num, column=col_idx, value=val)
+                        fill_count += 1
+                    else:
+                        ws.cell(row=row_num, column=col_idx, value="Not Variant")
+                        fill_count += 1
+                
+                elif source_type == "source":
+                    if source_map.get(source_val):
+                        col = source_map[source_val]
+                        val = src_row.get(col, "")
+                        if pd.notna(val):
+                            ws.cell(row=row_num, column=col_idx, value=val)
+                            fill_count += 1
+            
+            # Variant Grouping 特殊逻辑
+            if "Variant Grouping 1" in col_indices:
+                if variant_type != "Not Variant":
+                    ws.cell(row=row_num, column=col_indices["Variant Grouping 1"], value="Color")
+                    fill_count += 1
+            
+            if "Variant Attribute Name On Site 1" in col_indices:
+                if variant_type != "Not Variant":
+                    ws.cell(row=row_num, column=col_indices["Variant Attribute Name On Site 1"], value="Finish")
+                    fill_count += 1
+            
+            if "Variant Grouping 2" in col_indices:
+                if variant_type != "Not Variant":
+                    ws.cell(row=row_num, column=col_indices["Variant Grouping 2"], value="Size")
+                    fill_count += 1
+            
+            if "Variant Attribute Name On Site 2" in col_indices:
+                if variant_type != "Not Variant":
+                    ws.cell(row=row_num, column=col_indices["Variant Attribute Name On Site 2"], value="Dimensions")
+                    fill_count += 1
+            
+            row_num += 1
+            filled_count += 1
+        
+        # ========== 保存输出文件 ==========
+        output_filename = f"output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
+        output_path = os.path.join(OUTPUT_FOLDER, output_filename)
+        wb.save(output_path)
+        
+        # 清理上传文件
+        try:
+            os.remove(source_path)
+        except:
+            pass
+        
+        return jsonify({
+            'success': True,
+            'filled_count': filled_count,
+            'download_url': f'/download/{output_filename}',
+            'message': f'成功追加 {filled_count} 行数据'
+        })
+        
+    except Exception as e:
+        return jsonify({'error': str(e)}), 500
+
+
+@app.route('/download/<filename>')
+def download(filename):
+    """下载生成的文件"""
+    file_path = os.path.join(OUTPUT_FOLDER, filename)
+    if not os.path.exists(file_path):
+        return '文件不存在', 404
+    return send_file(file_path, as_attachment=True, download_name=filename)
+
+
+if __name__ == '__main__':
+    # 确保有默认模板
+    default_template = os.path.join(TEMPLATES_FOLDER, '模板.xlsx')
+    if not os.path.exists(default_template):
+        # 创建空模板提示
+        print(f'⚠️ 请将模板文件放入 {TEMPLATES_FOLDER} 目录')
+    
+    app.run(host='0.0.0.0', port=5000, debug=True)