|
@@ -5,11 +5,7 @@
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>Excel 自动填表工具</title>
|
|
<title>Excel 自动填表工具</title>
|
|
|
<style>
|
|
<style>
|
|
|
- * {
|
|
|
|
|
- margin: 0;
|
|
|
|
|
- padding: 0;
|
|
|
|
|
- box-sizing: border-box;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ * { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
body {
|
|
body {
|
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
|
background: #f5f7fa;
|
|
background: #f5f7fa;
|
|
@@ -38,6 +34,11 @@
|
|
|
margin-bottom: 24px;
|
|
margin-bottom: 24px;
|
|
|
border-bottom: 1px solid #eee;
|
|
border-bottom: 1px solid #eee;
|
|
|
padding-bottom: 16px;
|
|
padding-bottom: 16px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 10px;
|
|
|
}
|
|
}
|
|
|
.form-group {
|
|
.form-group {
|
|
|
margin-bottom: 18px;
|
|
margin-bottom: 18px;
|
|
@@ -61,9 +62,9 @@
|
|
|
}
|
|
}
|
|
|
.form-row .form-group {
|
|
.form-row .form-group {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
- min-width: 180px;
|
|
|
|
|
|
|
+ min-width: 200px;
|
|
|
}
|
|
}
|
|
|
- select, input[type="file"], input[type="number"] {
|
|
|
|
|
|
|
+ select, input[type="file"] {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
padding: 10px 12px;
|
|
padding: 10px 12px;
|
|
|
border: 1px solid #d1d5db;
|
|
border: 1px solid #d1d5db;
|
|
@@ -77,14 +78,48 @@
|
|
|
outline: none;
|
|
outline: none;
|
|
|
box-shadow: 0 0 0 3px rgba(79, 140, 247, 0.15);
|
|
box-shadow: 0 0 0 3px rgba(79, 140, 247, 0.15);
|
|
|
}
|
|
}
|
|
|
|
|
+ .template-options {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ }
|
|
|
|
|
+ .template-option {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 120px;
|
|
|
|
|
+ padding: 14px 20px;
|
|
|
|
|
+ border: 2px solid #e5e7eb;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ background: #fafbfc;
|
|
|
|
|
+ }
|
|
|
|
|
+ .template-option:hover {
|
|
|
|
|
+ border-color: #4f8cf7;
|
|
|
|
|
+ background: #f0f4ff;
|
|
|
|
|
+ }
|
|
|
|
|
+ .template-option.active {
|
|
|
|
|
+ border-color: #4f8cf7;
|
|
|
|
|
+ background: #e8effe;
|
|
|
|
|
+ box-shadow: 0 0 0 3px rgba(79, 140, 247, 0.2);
|
|
|
|
|
+ }
|
|
|
|
|
+ .template-option .badge {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 400;
|
|
|
|
|
+ color: #888;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ }
|
|
|
.file-upload-wrapper {
|
|
.file-upload-wrapper {
|
|
|
- position: relative;
|
|
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: 12px;
|
|
gap: 12px;
|
|
|
}
|
|
}
|
|
|
.file-upload-wrapper input[type="file"] {
|
|
.file-upload-wrapper input[type="file"] {
|
|
|
padding: 8px;
|
|
padding: 8px;
|
|
|
|
|
+ flex: 1;
|
|
|
}
|
|
}
|
|
|
.file-name {
|
|
.file-name {
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
@@ -106,44 +141,16 @@
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
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;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .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 {
|
|
.actions {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
gap: 12px;
|
|
gap: 12px;
|
|
@@ -167,18 +174,10 @@
|
|
|
line-height: 1.6;
|
|
line-height: 1.6;
|
|
|
margin-top: 16px;
|
|
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;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .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 {
|
|
.preview-table-wrapper {
|
|
|
overflow-x: auto;
|
|
overflow-x: auto;
|
|
|
margin-top: 16px;
|
|
margin-top: 16px;
|
|
@@ -210,9 +209,7 @@
|
|
|
text-overflow: ellipsis;
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
white-space: nowrap;
|
|
|
}
|
|
}
|
|
|
- .preview-table tr:hover td {
|
|
|
|
|
- background: #f9fafb;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .preview-table tr:hover td { background: #f9fafb; }
|
|
|
.status-badge {
|
|
.status-badge {
|
|
|
display: inline-block;
|
|
display: inline-block;
|
|
|
padding: 4px 14px;
|
|
padding: 4px 14px;
|
|
@@ -220,41 +217,17 @@
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
|
font-weight: 500;
|
|
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;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .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-8 { margin-top: 8px; }
|
|
|
.mt-12 { margin-top: 12px; }
|
|
.mt-12 { margin-top: 12px; }
|
|
|
- .text-center { text-align: center; }
|
|
|
|
|
.text-sm { font-size: 13px; }
|
|
.text-sm { font-size: 13px; }
|
|
|
.text-muted { color: #888; }
|
|
.text-muted { color: #888; }
|
|
|
.hidden { display: none !important; }
|
|
.hidden { display: none !important; }
|
|
|
-
|
|
|
|
|
.download-section {
|
|
.download-section {
|
|
|
background: #ecfdf5;
|
|
background: #ecfdf5;
|
|
|
border: 1px solid #6ee7b7;
|
|
border: 1px solid #6ee7b7;
|
|
@@ -267,69 +240,98 @@
|
|
|
gap: 12px;
|
|
gap: 12px;
|
|
|
margin-top: 12px;
|
|
margin-top: 12px;
|
|
|
}
|
|
}
|
|
|
- .download-section.show {
|
|
|
|
|
- display: flex;
|
|
|
|
|
|
|
+ .download-section.show { display: flex; }
|
|
|
|
|
+ .config-info {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ padding: 6px 14px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ }
|
|
|
|
|
+ .config-info span { font-weight: 600; }
|
|
|
|
|
+ #aiLogContainer {
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ #aiLogArea {
|
|
|
|
|
+ background: #1a1a2e;
|
|
|
|
|
+ color: #e4e4e4;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 12px 16px;
|
|
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ max-height: 200px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ white-space: pre-wrap;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
}
|
|
}
|
|
|
|
|
+ #aiLogArea .log-success { color: #4ade80; }
|
|
|
|
|
+ #aiLogArea .log-error { color: #f87171; }
|
|
|
|
|
+ #aiLogArea .log-warn { color: #fbbf24; }
|
|
|
|
|
+ #aiLogArea .log-info { color: #60a5fa; }
|
|
|
</style>
|
|
</style>
|
|
|
</head>
|
|
</head>
|
|
|
<body>
|
|
<body>
|
|
|
<div class="container">
|
|
<div class="container">
|
|
|
<h1>📊 Excel 自动填表工具</h1>
|
|
<h1>📊 Excel 自动填表工具</h1>
|
|
|
<div class="subtitle">
|
|
<div class="subtitle">
|
|
|
- 选择模板 → 上传来源数据 → 预览 → 生成并下载
|
|
|
|
|
|
|
+ <span>选择模板 → 上传来源数据 → 预览 → 生成并下载</span>
|
|
|
<span class="status-badge idle" id="statusBadge">就绪</span>
|
|
<span class="status-badge idle" id="statusBadge">就绪</span>
|
|
|
</div>
|
|
</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 class="form-group">
|
|
|
|
|
+ <label>📁 选择平台模板</label>
|
|
|
|
|
+ <div class="template-options" id="templateOptions">
|
|
|
|
|
+ <div class="template-option active" data-key="wayfair" onclick="selectTemplate('wayfair')">
|
|
|
|
|
+ 🏢 Wayfair
|
|
|
|
|
+ <span class="badge">wayfair模板.xlsx</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="template-option" data-key="amazon" onclick="selectTemplate('amazon')">
|
|
|
|
|
+ 🛒 亚马逊
|
|
|
|
|
+ <span class="badge">亚马逊模板.xlsx</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="mt-8" id="configInfo">
|
|
|
|
|
+ <span class="config-info">✅ 已选择 <span>Wayfair</span> 模板 | 来源表头: 第 2 行 | 模板表头: 第 4 行</span>
|
|
|
</div>
|
|
</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 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>
|
|
|
|
|
+ <div class="form-group" style="margin-top: 12px;">
|
|
|
|
|
+ <label style="display:flex;align-items:center;gap:10px;cursor:pointer;">
|
|
|
|
|
+ <input type="checkbox" id="useAI" checked>
|
|
|
|
|
+ 🤖 启用 AI 自动填写(识别产品主图,自动填充属性)
|
|
|
|
|
+ <span class="hint">(需要产品主图URL)</span>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
<!-- 操作按钮 -->
|
|
<!-- 操作按钮 -->
|
|
|
<div class="actions">
|
|
<div class="actions">
|
|
|
- <button class="btn btn-primary" id="previewBtn">🔍 预览数据</button>
|
|
|
|
|
|
|
+ <button class="btn btn-primary" id="previewBtn" disabled>🔍 预览数据</button>
|
|
|
<button class="btn btn-success" id="generateBtn" disabled>🚀 生成并下载</button>
|
|
<button class="btn btn-success" id="generateBtn" disabled>🚀 生成并下载</button>
|
|
|
<button class="btn btn-secondary" id="clearLogBtn">🗑️ 清空日志</button>
|
|
<button class="btn btn-secondary" id="clearLogBtn">🗑️ 清空日志</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 状态信息 -->
|
|
|
|
|
<div class="flex-between">
|
|
<div class="flex-between">
|
|
|
<span id="statusText" class="text-muted text-sm">请选择模板和来源文件</span>
|
|
<span id="statusText" class="text-muted text-sm">请选择模板和来源文件</span>
|
|
|
<span id="rowCount" class="text-muted text-sm"></span>
|
|
<span id="rowCount" class="text-muted text-sm"></span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 预览表格 -->
|
|
|
|
|
|
|
+ <!-- 预览 -->
|
|
|
<div id="previewContainer" class="hidden">
|
|
<div id="previewContainer" class="hidden">
|
|
|
<div class="flex-between mt-8">
|
|
<div class="flex-between mt-8">
|
|
|
<span class="text-sm" style="font-weight:600;">📋 数据预览 (前10行)</span>
|
|
<span class="text-sm" style="font-weight:600;">📋 数据预览 (前10行)</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="preview-table-wrapper" id="previewWrapper">
|
|
|
|
|
|
|
+ <div class="preview-table-wrapper">
|
|
|
<table class="preview-table" id="previewTable">
|
|
<table class="preview-table" id="previewTable">
|
|
|
<thead id="previewHead"></thead>
|
|
<thead id="previewHead"></thead>
|
|
|
<tbody id="previewBody"></tbody>
|
|
<tbody id="previewBody"></tbody>
|
|
@@ -337,26 +339,31 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 下载区域 -->
|
|
|
|
|
|
|
+ <!-- 下载 -->
|
|
|
<div class="download-section" id="downloadSection">
|
|
<div class="download-section" id="downloadSection">
|
|
|
<span style="font-weight:600;color:#065f46;">✅ 生成完成!</span>
|
|
<span style="font-weight:600;color:#065f46;">✅ 生成完成!</span>
|
|
|
<span id="downloadInfo"></span>
|
|
<span id="downloadInfo"></span>
|
|
|
<a href="#" id="downloadLink" class="btn btn-success" style="text-decoration:none;">📥 下载文件</a>
|
|
<a href="#" id="downloadLink" class="btn btn-success" style="text-decoration:none;">📥 下载文件</a>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
|
|
+ <div id="aiLogContainer" class="hidden">
|
|
|
|
|
+ <div class="flex-between mt-12">
|
|
|
|
|
+ <span style="font-weight:600;font-size:14px;">🤖 AI 处理日志</span>
|
|
|
|
|
+ <span id="aiProgress" class="text-muted text-sm"></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="log-area" id="aiLogArea" style="max-height:200px;font-size:11px;margin-top:8px;">
|
|
|
|
|
+ <span class="log-info">💡 AI 日志将在这里显示...</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
<!-- 日志 -->
|
|
<!-- 日志 -->
|
|
|
<div class="log-area" id="logArea">
|
|
<div class="log-area" id="logArea">
|
|
|
- <span class="log-info">💡 系统就绪,等待操作...</span>
|
|
|
|
|
|
|
+ <span class="log-info">💡 系统就绪,请选择模板并上传来源文件</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
- // DOM 元素
|
|
|
|
|
- const templateSelect = document.getElementById('templateSelect');
|
|
|
|
|
|
|
+ const templateOptions = document.querySelectorAll('.template-option');
|
|
|
const sourceFile = document.getElementById('sourceFile');
|
|
const sourceFile = document.getElementById('sourceFile');
|
|
|
const fileName = document.getElementById('fileName');
|
|
const fileName = document.getElementById('fileName');
|
|
|
- const sourceHeader = document.getElementById('sourceHeader');
|
|
|
|
|
- const templateHeader = document.getElementById('templateHeader');
|
|
|
|
|
const previewBtn = document.getElementById('previewBtn');
|
|
const previewBtn = document.getElementById('previewBtn');
|
|
|
const generateBtn = document.getElementById('generateBtn');
|
|
const generateBtn = document.getElementById('generateBtn');
|
|
|
const clearLogBtn = document.getElementById('clearLogBtn');
|
|
const clearLogBtn = document.getElementById('clearLogBtn');
|
|
@@ -370,77 +377,89 @@
|
|
|
const downloadSection = document.getElementById('downloadSection');
|
|
const downloadSection = document.getElementById('downloadSection');
|
|
|
const downloadLink = document.getElementById('downloadLink');
|
|
const downloadLink = document.getElementById('downloadLink');
|
|
|
const downloadInfo = document.getElementById('downloadInfo');
|
|
const downloadInfo = document.getElementById('downloadInfo');
|
|
|
|
|
+ const configInfo = document.getElementById('configInfo');
|
|
|
|
|
|
|
|
|
|
+ let selectedTemplate = 'wayfair';
|
|
|
let sourcePath = null;
|
|
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;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const templateNames = { wayfair: 'Wayfair', amazon: '亚马逊' };
|
|
|
|
|
|
|
|
- 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');
|
|
|
|
|
|
|
+ function selectTemplate(key) {
|
|
|
|
|
+ selectedTemplate = key;
|
|
|
|
|
+ templateOptions.forEach(el => {
|
|
|
|
|
+ el.classList.toggle('active', el.dataset.key === key);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ fetch('/get_config')
|
|
|
|
|
+ .then(res => res.json())
|
|
|
|
|
+ .then(data => {
|
|
|
|
|
+ const config = data.config[key];
|
|
|
|
|
+ if (config) {
|
|
|
|
|
+ configInfo.innerHTML = `
|
|
|
|
|
+ <span class="config-info">
|
|
|
|
|
+ ✅ 已选择 <span>${templateNames[key]}</span> 模板
|
|
|
|
|
+ | 来源表头: 第 ${config.source_header} 行
|
|
|
|
|
+ | 模板表头: 第 ${config.template_header} 行
|
|
|
|
|
+ | 文件: ${config.file}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ checkReady();
|
|
|
|
|
+ log(`📁 选择模板: ${templateNames[key] || key}`, 'info');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 文件选择
|
|
|
|
|
sourceFile.addEventListener('change', function() {
|
|
sourceFile.addEventListener('change', function() {
|
|
|
if (this.files.length > 0) {
|
|
if (this.files.length > 0) {
|
|
|
fileName.textContent = this.files[0].name;
|
|
fileName.textContent = this.files[0].name;
|
|
|
|
|
+ log(`📄 选择文件: ${this.files[0].name}`, 'info');
|
|
|
} else {
|
|
} else {
|
|
|
fileName.textContent = '未选择文件';
|
|
fileName.textContent = '未选择文件';
|
|
|
}
|
|
}
|
|
|
checkReady();
|
|
checkReady();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- templateSelect.addEventListener('change', checkReady);
|
|
|
|
|
-
|
|
|
|
|
function checkReady() {
|
|
function checkReady() {
|
|
|
- const hasTemplate = templateSelect.value !== '';
|
|
|
|
|
|
|
+ const hasTemplate = selectedTemplate !== null;
|
|
|
const hasFile = sourceFile.files.length > 0;
|
|
const hasFile = sourceFile.files.length > 0;
|
|
|
previewBtn.disabled = !(hasTemplate && hasFile);
|
|
previewBtn.disabled = !(hasTemplate && hasFile);
|
|
|
generateBtn.disabled = true;
|
|
generateBtn.disabled = true;
|
|
|
if (hasTemplate && hasFile) {
|
|
if (hasTemplate && hasFile) {
|
|
|
setStatus('已选择模板和来源文件,点击预览', 'idle');
|
|
setStatus('已选择模板和来源文件,点击预览', 'idle');
|
|
|
|
|
+ } else if (!hasTemplate) {
|
|
|
|
|
+ setStatus('请选择模板', 'idle');
|
|
|
} else {
|
|
} else {
|
|
|
- setStatus('请选择模板和来源文件', 'idle');
|
|
|
|
|
|
|
+ setStatus('请选择来源文件', 'idle');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 预览
|
|
|
|
|
|
|
+ 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;
|
|
|
|
|
+ const labels = { idle: '就绪', loading: '处理中...', success: '成功', error: '失败' };
|
|
|
|
|
+ statusBadge.textContent = labels[type] || '就绪';
|
|
|
|
|
+ statusBadge.className = `status-badge ${type}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function clearLog() {
|
|
|
|
|
+ logArea.innerHTML = '';
|
|
|
|
|
+ log('日志已清空', 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+ clearLogBtn.addEventListener('click', clearLog);
|
|
|
|
|
+
|
|
|
previewBtn.addEventListener('click', async function() {
|
|
previewBtn.addEventListener('click', async function() {
|
|
|
- const template = templateSelect.value;
|
|
|
|
|
const file = sourceFile.files[0];
|
|
const file = sourceFile.files[0];
|
|
|
- const sHeader = parseInt(sourceHeader.value) || 2;
|
|
|
|
|
-
|
|
|
|
|
- if (!template || !file) {
|
|
|
|
|
|
|
+ if (!selectedTemplate || !file) {
|
|
|
log('请选择模板和来源文件', 'warn');
|
|
log('请选择模板和来源文件', 'warn');
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
@@ -451,14 +470,10 @@
|
|
|
|
|
|
|
|
const formData = new FormData();
|
|
const formData = new FormData();
|
|
|
formData.append('source_file', file);
|
|
formData.append('source_file', file);
|
|
|
- formData.append('template_name', template);
|
|
|
|
|
- formData.append('source_header', sHeader);
|
|
|
|
|
|
|
+ formData.append('template_key', selectedTemplate);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const response = await fetch('/preview', {
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- body: formData
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ const response = await fetch('/preview', { method: 'POST', body: formData });
|
|
|
const result = await response.json();
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
if (result.error) {
|
|
if (result.error) {
|
|
@@ -473,11 +488,12 @@
|
|
|
setStatus(`预览成功,共 ${result.total_rows} 行`, 'success');
|
|
setStatus(`预览成功,共 ${result.total_rows} 行`, 'success');
|
|
|
rowCount.textContent = `共 ${result.total_rows} 行`;
|
|
rowCount.textContent = `共 ${result.total_rows} 行`;
|
|
|
|
|
|
|
|
- // 渲染表格
|
|
|
|
|
- renderPreview(result.columns, result.data);
|
|
|
|
|
|
|
+ previewHead.innerHTML = `<tr>${result.columns.map(c => `<th>${escapeHtml(c)}</th>`).join('')}</tr>`;
|
|
|
|
|
+ previewBody.innerHTML = result.data.map(row => {
|
|
|
|
|
+ return `<tr>${result.columns.map(c => `<td title="${escapeHtml(String(row[c] || ''))}">${escapeHtml(String(row[c] || ''))}</td>`).join('')}</tr>`;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+
|
|
|
previewContainer.classList.remove('hidden');
|
|
previewContainer.classList.remove('hidden');
|
|
|
-
|
|
|
|
|
- // 启用生成按钮
|
|
|
|
|
generateBtn.disabled = false;
|
|
generateBtn.disabled = false;
|
|
|
|
|
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
@@ -488,66 +504,137 @@
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- 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) {
|
|
function escapeHtml(text) {
|
|
|
const div = document.createElement('div');
|
|
const div = document.createElement('div');
|
|
|
div.textContent = text;
|
|
div.textContent = text;
|
|
|
return div.innerHTML;
|
|
return div.innerHTML;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 生成
|
|
|
|
|
generateBtn.addEventListener('click', async function() {
|
|
generateBtn.addEventListener('click', async function() {
|
|
|
if (!sourcePath) {
|
|
if (!sourcePath) {
|
|
|
log('请先预览数据', 'warn');
|
|
log('请先预览数据', 'warn');
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const template = templateSelect.value;
|
|
|
|
|
- const sHeader = parseInt(sourceHeader.value) || 2;
|
|
|
|
|
- const tHeader = parseInt(templateHeader.value) || 4;
|
|
|
|
|
-
|
|
|
|
|
this.disabled = true;
|
|
this.disabled = true;
|
|
|
previewBtn.disabled = true;
|
|
previewBtn.disabled = true;
|
|
|
setStatus('正在生成...', 'loading');
|
|
setStatus('正在生成...', 'loading');
|
|
|
log('🚀 开始生成并追加到模板...', 'info');
|
|
log('🚀 开始生成并追加到模板...', 'info');
|
|
|
|
|
|
|
|
|
|
+ // 显示AI日志区域
|
|
|
|
|
+ const aiLogContainer = document.getElementById('aiLogContainer');
|
|
|
|
|
+ if (aiLogContainer) {
|
|
|
|
|
+ aiLogContainer.classList.remove('hidden');
|
|
|
|
|
+ document.getElementById('aiLogArea').innerHTML = '<span class="log-info">⏳ AI处理中,请等待...</span>';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const formData = new FormData();
|
|
const formData = new FormData();
|
|
|
formData.append('source_path', sourcePath);
|
|
formData.append('source_path', sourcePath);
|
|
|
- formData.append('template_name', template);
|
|
|
|
|
- formData.append('source_header', sHeader);
|
|
|
|
|
- formData.append('header_row', tHeader);
|
|
|
|
|
|
|
+ formData.append('template_key', selectedTemplate);
|
|
|
|
|
+ const useAICheckbox = document.getElementById('useAI');
|
|
|
|
|
+ formData.append('use_ai', useAICheckbox ? useAICheckbox.checked : false);
|
|
|
|
|
+
|
|
|
|
|
+ // 开始轮询AI日志
|
|
|
|
|
+ let logInterval = null;
|
|
|
|
|
+ let pollCount = 0;
|
|
|
|
|
+ const maxPolls = 300; // 最多轮询300次(10分钟)
|
|
|
|
|
+
|
|
|
|
|
+ function startAILogPolling() {
|
|
|
|
|
+ if (logInterval) clearInterval(logInterval);
|
|
|
|
|
+
|
|
|
|
|
+ logInterval = setInterval(async function() {
|
|
|
|
|
+ pollCount++;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/get_ai_logs');
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ if (data.logs && data.logs.length > 0) {
|
|
|
|
|
+ updateAILogDisplay(data.logs);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 如果轮询超过最大次数,自动停止
|
|
|
|
|
+ if (pollCount >= maxPolls) {
|
|
|
|
|
+ clearInterval(logInterval);
|
|
|
|
|
+ logInterval = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ // 静默失败,不中断轮询
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 2000); // 每2秒刷新一次
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function updateAILogDisplay(logs) {
|
|
|
|
|
+ const aiLogArea = document.getElementById('aiLogArea');
|
|
|
|
|
+ if (!aiLogArea) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 只显示最近的日志(避免太多)
|
|
|
|
|
+ const recentLogs = logs.slice(-50);
|
|
|
|
|
+ aiLogArea.innerHTML = recentLogs.map(log => {
|
|
|
|
|
+ const colors = {
|
|
|
|
|
+ info: 'log-info',
|
|
|
|
|
+ success: 'log-success',
|
|
|
|
|
+ error: 'log-error',
|
|
|
|
|
+ warn: 'log-warn'
|
|
|
|
|
+ };
|
|
|
|
|
+ const prefix = {
|
|
|
|
|
+ info: 'ℹ️',
|
|
|
|
|
+ success: '✅',
|
|
|
|
|
+ error: '❌',
|
|
|
|
|
+ warn: '⚠️'
|
|
|
|
|
+ };
|
|
|
|
|
+ const level = log.level || 'info';
|
|
|
|
|
+ return `<div><span style="color:#888;">[${log.time}]</span> <span class="${colors[level] || 'log-info'}">${prefix[level] || 'ℹ️'} ${log.message}</span></div>`;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+ aiLogArea.scrollTop = aiLogArea.scrollHeight;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 开始轮询
|
|
|
|
|
+ startAILogPolling();
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const response = await fetch('/generate', {
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- body: formData
|
|
|
|
|
|
|
+ const response = await fetch('/generate', {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ body: formData
|
|
|
});
|
|
});
|
|
|
const result = await response.json();
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
+ // 停止轮询
|
|
|
|
|
+ if (logInterval) {
|
|
|
|
|
+ clearInterval(logInterval);
|
|
|
|
|
+ logInterval = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (result.error) {
|
|
if (result.error) {
|
|
|
log(`❌ 生成失败: ${result.error}`, 'error');
|
|
log(`❌ 生成失败: ${result.error}`, 'error');
|
|
|
setStatus('生成失败', 'error');
|
|
setStatus('生成失败', 'error');
|
|
|
|
|
+ // 显示错误日志
|
|
|
|
|
+ if (result.logs) {
|
|
|
|
|
+ updateAILogDisplay(result.logs);
|
|
|
|
|
+ }
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
log(`✅ ${result.message}`, 'success');
|
|
log(`✅ ${result.message}`, 'success');
|
|
|
setStatus(`生成成功,追加 ${result.filled_count} 行`, 'success');
|
|
setStatus(`生成成功,追加 ${result.filled_count} 行`, 'success');
|
|
|
|
|
|
|
|
- // 显示下载
|
|
|
|
|
downloadSection.classList.add('show');
|
|
downloadSection.classList.add('show');
|
|
|
downloadLink.href = result.download_url;
|
|
downloadLink.href = result.download_url;
|
|
|
downloadInfo.textContent = `已追加 ${result.filled_count} 行数据`;
|
|
downloadInfo.textContent = `已追加 ${result.filled_count} 行数据`;
|
|
|
|
|
|
|
|
|
|
+ // 显示最终AI日志
|
|
|
|
|
+ if (result.logs) {
|
|
|
|
|
+ updateAILogDisplay(result.logs);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果AI处理了数据,显示成功信息
|
|
|
|
|
+ if (result.ai_used && result.ai_rows > 0) {
|
|
|
|
|
+ document.getElementById('aiProgress').textContent = `✅ AI识别完成,成功 ${result.ai_rows} 行`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
|
|
+ // 停止轮询
|
|
|
|
|
+ if (logInterval) {
|
|
|
|
|
+ clearInterval(logInterval);
|
|
|
|
|
+ logInterval = null;
|
|
|
|
|
+ }
|
|
|
log(`❌ 请求失败: ${err.message}`, 'error');
|
|
log(`❌ 请求失败: ${err.message}`, 'error');
|
|
|
setStatus('生成失败', 'error');
|
|
setStatus('生成失败', 'error');
|
|
|
} finally {
|
|
} finally {
|
|
@@ -556,12 +643,9 @@
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 清空日志
|
|
|
|
|
- clearLogBtn.addEventListener('click', clearLog);
|
|
|
|
|
-
|
|
|
|
|
- // 初始检查
|
|
|
|
|
- checkReady();
|
|
|
|
|
- log('💡 请选择模板和来源Excel文件,然后点击"预览数据"', 'info');
|
|
|
|
|
|
|
+ // 默认选中 Wayfair
|
|
|
|
|
+ selectTemplate('wayfair');
|
|
|
|
|
+ log('💡 请上传来源Excel文件,然后点击"预览数据"', 'info');
|
|
|
</script>
|
|
</script>
|
|
|
</body>
|
|
</body>
|
|
|
</html>
|
|
</html>
|