Files
crawler-plugin/app/web_source/brand.html
铭坤 cb34788e53 modified: "web_source/brand-\346\227\247.html"
modified:   web_source/brand.html
	modified:   web_source/templates_backup/brand.html
2026-04-07 14:10:33 +08:00

1071 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>亚马逊 - 数富AI</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Microsoft YaHei", "Noto Serif SC", "SimSun", "Songti SC", "Times New Roman", serif;
background: rgba(13, 13, 13, 1);
color: #e0e0e0;
overflow: hidden;
height: 100vh;
font-weight: bold;
}
.top-bar {
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
border-bottom: 1px solid #2a2a2a;
}
.logo-area {
display: flex;
align-items: center;
gap: 10px;
}
.top-bar .app-name { font-size: 18px; font-weight: 600; color: #fff; }
.btn-home {
font-size: 13px;
color: #999;
text-decoration: none;
padding: 8px 12px;
border-radius: 6px;
transition: all 0.2s;
}
.btn-home:hover { color: #fff; background: #2a2a2a; }
.nav-tabs {
display: flex;
align-items: center;
gap: 12px;
}
.nav-tab-group {
display: flex;
align-items: center;
gap: 12px;
}
.nav-dropdown {
position: relative;
}
.nav-dropdown-trigger {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-width: 112px;
height: 38px;
padding: 0 16px;
border-radius: 10px;
border: 1px solid transparent;
color: #999;
background: transparent;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.nav-dropdown:hover .nav-dropdown-trigger,
.nav-dropdown.active .nav-dropdown-trigger {
color: #fff;
background: #2a2a2a;
}
.nav-dropdown.active .nav-dropdown-trigger {
color: #3498db;
background: rgba(52, 152, 219, 0.2);
border-color: rgba(52, 152, 219, 0.3);
}
.nav-dropdown-arrow {
font-size: 11px;
line-height: 1;
}
.nav-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
min-width: 220px;
padding: 8px;
border: 1px solid #2a2a2a;
border-radius: 12px;
background: #171717;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
display: none;
z-index: 20;
}
.nav-dropdown:hover .nav-dropdown-menu {
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-dropdown-item {
display: flex;
align-items: center;
min-height: 38px;
padding: 0 12px;
border-radius: 8px;
color: #b8b8b8;
font-size: 13px;
font-weight: 600;
text-decoration: none;
transition: all 0.2s ease;
}
.nav-dropdown-item:hover {
color: #fff;
background: #2a2a2a;
}
.nav-dropdown-item.active {
color: #3498db;
background: rgba(52, 152, 219, 0.2);
}
.top-right {
display: flex;
align-items: center;
gap: 8px;
}
.main-content {
display: flex;
height: calc(100vh - 56px);
}
/* 栏目内容容器:每个栏目一个 .tab-panel通过 data-panel 与导航对应 */
.tab-panel {
display: none;
flex: 1;
min-width: 0;
min-height: 0;
}
.tab-panel.active {
display: flex;
}
.left-panel {
width: 380px;
background: #1e1e1e;
padding: 20px;
overflow-y: auto;
border-right: 1px solid #2a2a2a;
}
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
background: #1a1a1a;
}
.section-title {
font-size: 13px;
color: #bbb;
margin-bottom: 10px;
}
.upload-zone {
border: 1px dashed #3a3a3a;
border-radius: 10px;
padding: 24px;
text-align: center;
background: #252525;
margin-bottom: 20px;
transition: all 0.2s;
}
.upload-zone:hover { border-color: #3498db; background: #2a2a2a; }
.upload-zone .hint { color: #888; font-size: 13px; margin-bottom: 12px; }
.upload-zone .btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.opt-btn {
padding: 8px 16px;
font-size: 13px;
color: #ccc;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.opt-btn:hover { color: #fff; background: #333; border-color: #3498db; color: #3498db; }
.selected-files {
margin-top: 12px;
font-size: 12px;
color: #888;
max-height: 72px;
overflow-y: auto;
}
.selected-files span { display: block; margin: 4px 0; word-break: break-all; }
.option-group { margin-bottom: 20px; }
.radio-item {
display: flex;
align-items: flex-start;
gap: 10px;
cursor: pointer;
padding: 10px 12px;
border-radius: 8px;
background: #252525;
border: 1px solid #2a2a2a;
margin-bottom: 8px;
}
.radio-item:hover { background: #2a2a2a; }
.radio-item input { margin-top: 3px; }
.radio-item .label { font-weight: 500; color: #e0e0e0; font-size: 13px; }
.radio-item .desc { font-size: 12px; color: #888; margin-top: 2px; font-weight: normal; }
.run-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
}
.btn-run {
padding: 10px 22px;
background: #3498db;
color: #fff;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.btn-run:hover { background: #2980b9; }
.btn-run:disabled { background: #555; cursor: not-allowed; color: #999; }
.loading-msg { color: #3498db; font-size: 13px; }
.template-download-row {
margin-top: 16px;
padding: 14px 16px;
background: linear-gradient(135deg, #252525 0%, #1e2a1e 100%);
border: 1px solid #2a3a2a;
border-radius: 10px;
display: flex;
align-items: center;
gap: 14px;
transition: all 0.25s ease;
cursor: pointer;
}
.template-download-row:hover {
border-color: #2e7d32;
background: linear-gradient(135deg, #2a2a2a 0%, #243324 100%);
box-shadow: 0 4px 12px rgba(46, 125, 50, 0.15);
}
.template-download-row:hover .template-download-text .title { color: #2ecc71; }
.template-download-row:hover .template-download-icon {
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(46, 125, 50, 0.45), inset 0 1px 0 rgba(255,255,255,0.15);
}
.template-download-icon {
width: 48px;
height: 48px;
flex-shrink: 0;
border-radius: 10px;
background: linear-gradient(145deg, #1b5e20 0%, #2e7d32 50%, #388e3c 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(46, 125, 50, 0.35), inset 0 1px 0 rgba(255,255,255,0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.template-download-icon svg {
width: 28px;
height: 28px;
}
.template-download-text .title { font-size: 14px; color: #e0e0e0; font-weight: 600; display: block; }
.template-download-text .hint { font-size: 12px; color: #888; margin-top: 2px; font-weight: normal; }
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid #2a2a2a;
font-size: 15px;
font-weight: 600;
color: #ddd;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.panel-header-title {
flex: 1;
}
.btn-refresh-tasks {
padding: 6px 14px;
font-size: 12px;
border-radius: 6px;
border: 1px solid #3a3a3a;
background: #2a2a2a;
color: #ccc;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.btn-refresh-tasks:hover {
background: #333;
border-color: #3498db;
color: #3498db;
}
.task-list-wrap {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.task-list { list-style: none; }
.task-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border: 1px solid #2a2a2a;
border-radius: 8px;
margin-bottom: 10px;
background: #1e1e1e;
}
.task-item .left { flex: 1; min-width: 0; }
.task-item .id { font-weight: 600; color: #e0e0e0; font-size: 13px; }
.task-item .files { font-size: 12px; color: #888; margin-top: 4px; }
.task-item .time { font-size: 12px; color: #666; margin-top: 2px; }
.task-item .status {
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
margin-right: 12px;
}
.task-item .status.pending { background: rgba(255,193,7,0.2); color: #d4a500; }
.task-item .status.running { background: rgba(52,152,219,0.2); color: #3498db; }
.task-item .status.success { background: rgba(46,204,113,0.2); color: #2ecc71; }
.task-item .status.failed { background: rgba(231,76,60,0.2); color: #e74c3c; }
.task-item .status.cancelled { background: rgba(149,165,166,0.2); color: #95a5a6; }
.task-item .progress-wrap { margin-top: 8px; margin-right: 8px; flex-shrink: 0; width: 120px; }
.task-item .progress-bar-bg { height: 6px; background: #2a2a2a; border-radius: 3px; overflow: hidden; }
.task-item .progress-bar-fill { height: 100%; background: #3498db; border-radius: 3px; transition: width 0.2s; }
.task-item .btn-cancel, .task-item .btn-delete {
padding: 6px 12px; font-size: 12px; border-radius: 6px; cursor: pointer; border: none; margin-left: 6px;
transition: all 0.2s;
}
.task-item .btn-cancel { background: #e67e22; color: #fff; }
.task-item .btn-cancel:hover { background: #d35400; }
.task-item .btn-delete { background: #444; color: #ccc; }
.task-item .btn-delete:hover { background: #e74c3c; color: #fff; }
.task-item .task-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.run-row .progress-wrap { margin-top: 10px; width: 100%; max-width: 280px; }
.run-row .progress-bar-bg { height: 8px; background: #2a2a2a; border-radius: 4px; overflow: hidden; }
.run-row .progress-bar-fill { height: 100%; background: #3498db; border-radius: 4px; transition: width 0.2s; }
.run-row .btn-cancel-run { margin-left: 12px; padding: 8px 16px; background: #e67e22; color: #fff; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; }
.run-row .btn-cancel-run:hover { background: #d35400; }
.task-item .download {
padding: 6px 14px;
background: #3498db;
color: #fff;
border: none;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
text-decoration: none;
white-space: nowrap;
transition: all 0.2s;
}
.task-item .download:hover { background: #2980b9; color: #fff; }
.task-item .download.hide { display: none; }
.empty-tasks { text-align: center; color: #666; padding: 32px; font-size: 13px; }
.toast {
position: fixed;
bottom: 32px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.85);
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s;
border: 1px solid #333;
}
.toast.show { opacity: 1; }
</style>
</head>
<body>
<header class="top-bar">
<div class="logo-area">
<span class="app-name">数富AI-亚马逊</span>
<a href="/home" class="btn-home">返回首页</a>
</div>
<nav class="nav-tabs">
<div class="nav-tab-group">
<div class="nav-dropdown active">
<button type="button" class="nav-dropdown-trigger">
<span>前端工具</span>
<span class="nav-dropdown-arrow"></span>
</button>
<div class="nav-dropdown-menu">
<button type="button" class="nav-dropdown-item active" data-panel="brandCheck">品牌检测</button>
<a class="nav-dropdown-item" href="/new_web_source/dedupe.html">数据去重</a>
<a class="nav-dropdown-item" href="/new_web_source/split.html">数据拆分</a>
<a class="nav-dropdown-item" href="/new_web_source/convert.html">格式转换</a>
</div>
</div>
<div class="nav-dropdown">
<button type="button" class="nav-dropdown-trigger">
<span>运营工具</span>
<span class="nav-dropdown-arrow"></span>
</button>
<div class="nav-dropdown-menu">
<a class="nav-dropdown-item" href="/new_web_source/delete-brand.html">删除指定ASIN和品牌</a>
<a class="nav-dropdown-item" href="/new_web_source/product-risk.html">商品风险解决</a>
</div>
</div>
</div>
</nav>
<div class="top-right"></div>
</header>
<div class="main-content">
<!-- 品牌检测栏目容器;后续增加栏目时复制此结构,改 data-panel 与 id并去掉 .active -->
<div class="tab-panel active" data-panel="brandCheck" id="panel-brandCheck">
<aside class="left-panel">
<div class="section-title">选择文件</div>
<div class="upload-zone" id="uploadZone">
<div class="hint">选择 .xlsx 文件或文件夹(将读取该文件夹下所有 xlsx</div>
<div class="btns">
<button type="button" class="opt-btn" id="btnSelectFiles">选择 Excel 文件</button>
<button type="button" class="opt-btn" id="btnSelectFolder">选择文件夹</button>
</div>
<div class="selected-files" id="selectedFiles"></div>
</div>
<div class="section-title">运行方式</div>
<div class="option-group">
<label class="radio-item">
<input type="radio" name="runMode" value="immediate" checked>
<div>
<span class="label">立即运行</span>
<div class="desc">立刻执行,执行完成后可下载结果</div>
</div>
</label>
<label class="radio-item">
<input type="radio" name="runMode" value="task">
<div>
<span class="label">添加任务</span>
<div class="desc">数量较多时建议添加任务,后台执行,可在右侧任务队列查看状态与下载结果</div>
</div>
</label>
</div>
<div class="section-title">匹配方式</div>
<div class="option-group">
<label class="radio-item">
<input type="radio" name="matchStrategy" value="Terms" checked>
<div>
<span class="label">精确匹配表达式</span>
<div class="desc">使用精确匹配,适合严格匹配品牌名称</div>
</div>
</label>
<label class="radio-item">
<input type="radio" name="matchStrategy" value="Simple">
<div>
<span class="label">嵌入(结果包含输入的词语)</span>
<div class="desc">使用嵌入策略,结果中包含输入的品牌词语</div>
</div>
</label>
</div>
<div class="run-row" id="runRow">
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:10px;">
<button type="button" class="btn-run" id="btnRun">立即运行</button>
<span class="loading-msg" id="loadingMsg" style="display:none;">生成中,请稍候…</span>
<div class="progress-wrap" id="runProgressWrap" style="display:none;">
<div class="progress-bar-bg"><div class="progress-bar-fill" id="runProgressFill" style="width:0%;"></div></div>
<span class="hint" style="font-size:12px;color:#888;" id="runProgressText">0 / 0</span>
</div>
<!-- 当前文件行级进度(不经数据库中转,只读内存接口) -->
<div class="progress-wrap" id="runLineProgressWrap" style="display:none;">
<div class="progress-bar-bg"><div class="progress-bar-fill" id="runLineProgressFill" style="width:0%;"></div></div>
<span class="hint" style="font-size:12px;color:#888;" id="runLineProgressText">0 / 0</span>
</div>
<button type="button" class="btn-cancel-run" id="btnCancelRun" style="display:none;">取消生成</button>
</div>
</div>
<div class="template-download-row" id="templateDownloadRow" role="button" tabindex="0" title="下载品牌文档格式模板(选择保存位置)">
<span class="template-download-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 2v6h6" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 13h8M8 17h5" stroke="rgba(255,255,255,0.85)" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</span>
<span class="template-download-text">
<span class="title">模板下载</span>
<span class="hint">品牌文档格式_模板.xlsx · 点击选择保存位置</span>
</span>
</div>
<div class="template-download-row" id="templateDownloadRow2" role="button" tabindex="0" title="下载模板2以文件夹方式上传">
<span class="template-download-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 2v6h6" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 7v10h4v-4h2v4h8v-4h2v4h2V7H3z" stroke="rgba(255,255,255,0.85)" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</span>
<span class="template-download-text">
<span class="title">模板2下载</span>
<span class="hint">以文件夹方式上传.zip · 点击下载</span>
</span>
</div>
</aside>
<div class="right-panel">
<div class="panel-header">
<span class="panel-header-title">任务队列</span>
<button type="button" class="btn-refresh-tasks" id="btnRefreshTasks">刷新任务状态</button>
</div>
<div class="task-list-wrap">
<ul class="task-list" id="taskList"></ul>
<div class="empty-tasks" id="emptyTasks">暂无任务</div>
</div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
(function() {
var selectedPaths = [];
var cachedTasks = [];
var api = window.pywebview && window.pywebview.api;
function getUid() {
return localStorage.getItem('uid') || '';
}
function withUidHeaders(headers) {
headers = headers || {};
headers.uid = getUid();
return headers;
}
// 顶部栏目切换:点击当前页菜单项时显示对应 data-panel 的 tab-panel
document.querySelectorAll('.nav-dropdown-item[data-panel]').forEach(function(tab) {
tab.addEventListener('click', function() {
var panelId = this.getAttribute('data-panel');
if (!panelId) return;
document.querySelectorAll('.nav-dropdown-item[data-panel]').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.tab-panel').forEach(function(p) {
p.classList.toggle('active', p.getAttribute('data-panel') === panelId);
});
this.classList.add('active');
});
});
function showToast(msg) {
var el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
setTimeout(function() { el.classList.remove('show'); }, 2500);
}
function renderSelected() {
var el = document.getElementById('selectedFiles');
if (selectedPaths.length === 0) {
el.innerHTML = '';
return;
}
el.innerHTML = '已选 <b>' + selectedPaths.length + '</b> 个文件:<br>' +
selectedPaths.slice(0, 20).map(function(p) {
var name = p.split(/[/\\]/).pop();
return '<span title="' + p + '">' + name + '</span>';
}).join('') +
(selectedPaths.length > 20 ? '<span>… 等 ' + selectedPaths.length + ' 个</span>' : '');
}
document.getElementById('btnSelectFiles').onclick = async function(e) {
e.stopPropagation();
if (!api || !api.select_brand_xlsx_files) {
showToast('当前环境不支持文件选择,请在本机客户端中打开');
return;
}
try {
var paths = await api.select_brand_xlsx_files();
if (paths && paths.length) {
selectedPaths = paths;
renderSelected();
showToast('已选择 ' + paths.length + ' 个文件');
}
} catch (err) {
showToast('选择失败:' + (err.message || err));
}
};
document.getElementById('templateDownloadRow').onclick = async function(e) {
e.preventDefault();
if (api && api.save_template_xlsx) {
try {
var res = await api.save_template_xlsx();
if (res && res.success) {
showToast('模板已保存至:' + (res.path || ''));
} else {
showToast(res && res.error ? res.error : '保存失败');
}
} catch (err) {
showToast('下载失败:' + (err.message || err));
}
return;
}
window.location.href = '/static/品牌文档格式_模板.xlsx';
};
document.getElementById('templateDownloadRow').onkeydown = function(e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.click(); }
};
document.getElementById('templateDownloadRow2').onclick = async function(e) {
e.preventDefault();
if (api && api.save_template_zip) {
try {
var res = await api.save_template_zip();
if (res && res.success) {
showToast('模板已保存至:' + (res.path || ''));
} else {
showToast(res && res.error ? res.error : '保存失败');
}
} catch (err) {
showToast('下载失败:' + (err.message || err));
}
return;
}
window.location.href = '/static/模板2-以文件夹方式上传.zip';
};
document.getElementById('templateDownloadRow2').onkeydown = function(e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.click(); }
};
document.getElementById('btnSelectFolder').onclick =async function(e) {
e.stopPropagation();
if (!api || !api.select_brand_folder) {
showToast('当前环境不支持文件夹选择,请在本机客户端中打开');
return;
}
try {
var folder = await api.select_brand_folder();
if (!folder) return;
fetch('/api/brand/expand-folder', {
method: 'POST',
headers: withUidHeaders({ 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }),
credentials: 'include',
body: JSON.stringify({ folder: folder })
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success && res.paths && res.paths.length) {
selectedPaths = res.paths;
renderSelected();
showToast('已选择文件夹内 ' + res.paths.length + ' 个 xlsx 文件');
} else {
showToast(res.error || '该文件夹下没有 xlsx 文件');
}
})
.catch(function() { showToast('请求失败'); });
} catch (err) {
showToast('选择失败:' + (err.message || err));
}
};
function getRunMode() {
var r = document.querySelector('input[name="runMode"]:checked');
return r ? r.value : 'immediate';
}
function getMatchStrategy() {
var r = document.querySelector('input[name="matchStrategy"]:checked');
return r ? r.value : 'Terms';
}
var pollTaskId = null;
var pollTimer = null;
var runTaskEventAbortController = null;
function stopPollAndResetRunUI() {
pollTaskId = null;
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
if (runTaskEventAbortController) {
try { runTaskEventAbortController.abort(); } catch (e) {}
runTaskEventAbortController = null;
}
var btn = document.getElementById('btnRun');
var loadingMsg = document.getElementById('loadingMsg');
var runProgressWrap = document.getElementById('runProgressWrap');
var runLineProgressWrap = document.getElementById('runLineProgressWrap');
var btnCancelRun = document.getElementById('btnCancelRun');
btn.disabled = false;
loadingMsg.style.display = 'none';
if (runProgressWrap) runProgressWrap.style.display = 'none';
if (runLineProgressWrap) runLineProgressWrap.style.display = 'none';
if (btnCancelRun) btnCancelRun.style.display = 'none';
}
function onRunTaskFinished(status) {
stopPollAndResetRunUI();
loadTasks();
if (status === 'success') showToast('生成完成,可下载结果');
else if (status === 'failed') showToast('生成失败');
else if (status === 'cancelled') showToast('已取消');
}
function pollRunTask(taskId) {
pollTaskId = taskId;
var runProgressFill = document.getElementById('runProgressFill');
var runProgressText = document.getElementById('runProgressText');
var runLineProgressWrap = document.getElementById('runLineProgressWrap');
var runLineProgressFill = document.getElementById('runLineProgressFill');
var runLineProgressText = document.getElementById('runLineProgressText');
var btnCancelRun = document.getElementById('btnCancelRun');
var runProgressWrap = document.getElementById('runProgressWrap');
runProgressWrap.style.display = 'block';
btnCancelRun.style.display = 'inline-block';
btnCancelRun.onclick = function() {
fetch('/api/brand/tasks/' + taskId + '/cancel', { method: 'POST', credentials: 'include', headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' }) })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) onRunTaskFinished('cancelled');
else showToast(res.error || '取消失败');
});
};
// 通过事件流订阅任务完成事件(使用 fetch 以便携带 uid 请求头)
var eventsUrl = '/api/brand/tasks/' + taskId + '/events';
runTaskEventAbortController = new AbortController();
(async function() {
try {
var resp = await fetch(eventsUrl, {
method: 'GET',
credentials: 'include',
headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'text/event-stream' }),
signal: runTaskEventAbortController.signal
});
if (!resp.ok || !resp.body) return;
var reader = resp.body.getReader();
var decoder = new TextDecoder('utf-8');
var buffer = '';
while (true) {
var r = await reader.read();
if (!r || r.done) break;
buffer += decoder.decode(r.value, { stream: true });
var parts = buffer.split(/\n\n/);
buffer = parts.pop();
var terminalStatus = null;
for (var i = 0; i < parts.length; i++) {
var part = parts[i];
if (!part) continue;
var dataLine = part.split('\n').find(function(l) { return l.indexOf('data:') === 0; });
if (!dataLine) continue;
var dataStr = dataLine.slice(5).trim();
if (!dataStr) continue;
try {
var data = JSON.parse(dataStr);
if (data && (data.status === 'success' || data.status === 'failed' || data.status === 'cancelled')) {
terminalStatus = data.status;
break;
}
} catch (err) {}
}
if (terminalStatus) {
try { runTaskEventAbortController.abort(); } catch (e2) {}
runTaskEventAbortController = null;
onRunTaskFinished(terminalStatus);
return;
}
}
} catch (err) {}
})();
function tick() {
if (!pollTaskId) return;
fetch('/api/brand/tasks/' + taskId, { credentials: 'include', headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' }) })
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.success || !res.task) return;
var t = res.task;
var total = t.progress_total || 0;
var cur = t.progress_current || 0;
if (runProgressFill) runProgressFill.style.width = total ? (100 * cur / total) + '%' : '30%';
if (runProgressText) {
var st = (t.status || '').toLowerCase();
if (st === 'running') {
runProgressText.textContent = total ? (cur + ' / ' + total) : '生成中…';
} else if (st === 'success') {
runProgressText.textContent = '已完成';
} else if (st === 'failed') {
runProgressText.textContent = '失败';
} else if (st === 'cancelled') {
runProgressText.textContent = '已取消';
} else {
runProgressText.textContent = '执行中…';
}
}
// 行级进度:从内存接口 /api/brand/tasks/<id>/line-progress 拉取
if (runLineProgressWrap && runLineProgressFill && runLineProgressText) {
var st2 = (t.status || '').toLowerCase();
if (st2 === 'running') {
fetch('/api/brand/tasks/' + taskId + '/line-progress', {
credentials: 'include',
headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' })
})
.then(function(r) { return r.json(); })
.then(function(res2) {
if (!res2.success || !res2.has_progress || !res2.info) {
runLineProgressWrap.style.display = 'none';
return;
}
var info = res2.info;
var totalLines = info.total_lines || 0;
var curLine = info.current_line || 0;
runLineProgressWrap.style.display = 'block';
runLineProgressFill.style.width = totalLines ? (100 * curLine / totalLines) + '%' : '0%';
runLineProgressText.textContent = totalLines ? (curLine + ' / ' + totalLines) : '当前文件处理中…';
})
.catch(function() {});
} else {
runLineProgressWrap.style.display = 'none';
}
}
// 终态也通过轮询兜底,确保 UI 一定被重置SSE 可能因网络未收到)
var st = (t.status || '').toLowerCase();
if (st === 'success' || st === 'failed' || st === 'cancelled') {
onRunTaskFinished(st);
}
});
}
tick();
pollTimer = setInterval(tick, 10000);
}
document.getElementById('btnRun').onclick = function() {
if (selectedPaths.length === 0) {
showToast('请先选择 Excel 文件或文件夹');
return;
}
var mode = getRunMode();
var strategy = getMatchStrategy();
var btn = document.getElementById('btnRun');
var loadingMsg = document.getElementById('loadingMsg');
if (mode === 'immediate') {
btn.disabled = true;
loadingMsg.style.display = 'inline';
fetch('/api/brand/run', {
method: 'POST',
headers: withUidHeaders({ 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }),
credentials: 'include',
body: JSON.stringify({ paths: selectedPaths, strategy: strategy, task_type: 1 })
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success && res.task_id) {
loadingMsg.style.display = 'none';
pollRunTask(res.task_id);
} else {
stopPollAndResetRunUI();
showToast(res.error || '运行失败');
}
})
.catch(function() {
stopPollAndResetRunUI();
showToast('请求失败或超时');
});
} else {
fetch('/api/brand/tasks', {
method: 'POST',
headers: withUidHeaders({ 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }),
credentials: 'include',
body: JSON.stringify({ paths: selectedPaths, strategy: strategy, task_type: 2 })
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
showToast('任务已添加,请在任务队列查看');
loadTasks();
} else {
showToast(res.error || '添加失败');
}
})
.catch(function() { showToast('请求失败'); });
}
};
function loadTasks() {
fetch('/api/brand/tasks', { credentials: 'include', headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' }) })
.then(function(r) { return r.json(); })
.then(function(res) {
var list = document.getElementById('taskList');
var empty = document.getElementById('emptyTasks');
if (!res.success || !res.items || res.items.length === 0) {
list.innerHTML = '';
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
cachedTasks = res.items;
list.innerHTML = res.items.map(function(t) {
var statusClass = (t.status || 'pending').toLowerCase();
var statusText = { pending: '等待中', running: '执行中', success: '已完成', failed: '失败', cancelled: '已取消' }[t.status] || t.status;
var filesDesc = (t.file_paths && t.file_paths.length) ? t.file_paths.length + ' 个文件' : '';
var total = t.progress_total || 0;
var cur = t.progress_current || 0;
var pct = total ? Math.min(100, (100 * cur / total)) : 0;
var progressHtml = (statusClass === 'running' && total > 0)
? '<div class="progress-wrap"><div class="progress-bar-bg"><div class="progress-bar-fill" style="width:' + pct + '%"></div></div><span class="hint" style="font-size:11px;color:#888;">' + cur + ' / ' + total + '</span></div>'
: '';
var hasZipUrl = t.result_paths && t.result_paths.zip_url;
var downloadHtml = hasZipUrl
? '<a class="download js-save-zip" href="#" data-task-id="' + t.id + '">下载结果</a>'
: '<span class="download hide"></span>';
var cancelBtn = statusClass === 'running'
? '<button type="button" class="btn-cancel js-cancel-task" data-task-id="' + t.id + '">取消</button>'
: '';
var deleteBtn = '<button type="button" class="btn-delete js-delete-task" data-task-id="' + t.id + '">删除</button>';
var titleDesc = (t.desc && String(t.desc).trim()) ? String(t.desc).trim() : ('任务 #' + t.id);
var displayDesc = titleDesc.length > 20 ? titleDesc.substring(0, 20) + '....' : titleDesc;
return '<li class="task-item">' +
'<div class="left">' +
'<span class="id" title="' + (titleDesc.replace(/"/g, '&quot;')) + '">' + displayDesc + '</span>' +
'<div class="files">' + filesDesc + '</div>' +
'<div class="time">' + (t.created_at || '') + '</div>' +
'</div>' +
'<div class="task-right">' +
progressHtml +
'<span class="status ' + statusClass + '">' + statusText + '</span>' +
cancelBtn +
downloadHtml +
deleteBtn +
'</div></li>';
}).join('');
})
.catch(function() {});
}
// 手动刷新任务状态按钮
var btnRefreshTasks = document.getElementById('btnRefreshTasks');
if (btnRefreshTasks) {
btnRefreshTasks.onclick = function () {
loadTasks();
showToast('任务状态已刷新');
};
}
document.getElementById('taskList').addEventListener('click', function(e) {
var target = e.target && e.target.closest && (e.target.closest('a.js-save-zip') || e.target.closest('button.js-cancel-task') || e.target.closest('button.js-delete-task'));
if (!target) return;
e.preventDefault();
var taskId = target.getAttribute('data-task-id');
if (target.classList && (target.classList.contains('js-cancel-task') || target.closest('.js-cancel-task'))) {
var btn = target.classList.contains('js-cancel-task') ? target : target.closest('.js-cancel-task');
if (!btn) return;
fetch('/api/brand/tasks/' + taskId + '/cancel', { method: 'POST', credentials: 'include', headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' }) })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) { showToast('已取消'); loadTasks(); } else { showToast(res.error || '取消失败'); }
});
return;
}
if (target.classList && (target.classList.contains('js-delete-task') || target.closest('.js-delete-task'))) {
var btn = target.classList.contains('js-delete-task') ? target : target.closest('.js-delete-task');
if (!btn) return;
fetch('/api/brand/tasks/' + taskId, { method: 'DELETE', credentials: 'include', headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' }) })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) { showToast('已删除'); loadTasks(); } else { showToast(res.error || '删除失败'); }
});
return;
}
var a = target.closest('a.js-save-zip') || target;
if (!a || !a.classList || !a.classList.contains('js-save-zip')) return;
var task = cachedTasks.filter(function(t) { return String(t.id) === String(taskId); })[0];
if (!task || !task.result_paths || !task.result_paths.zip_url) {
showToast('无法获取下载地址');
return;
}
var zipUrl = task.result_paths.zip_url;
var filename = 'brand_task_' + taskId + '.zip';
if (api && api.save_file_from_url) {
a.style.pointerEvents = 'none';
var done = function(ret) {
if (ret && ret.success) {
showToast('已保存:' + (ret.path || filename));
} else if (ret && ret.error && ret.error !== '用户取消') {
showToast('保存失败:' + ret.error);
}
a.style.pointerEvents = '';
};
try {
var ret = api.save_file_from_url(zipUrl, filename);
if (ret && typeof ret.then === 'function') {
ret.then(done).catch(function(err) { showToast('保存失败:' + (err && err.message || err)); a.style.pointerEvents = ''; });
} else {
done(ret);
}
} catch (err) {
showToast('保存失败:' + (err.message || err));
a.style.pointerEvents = '';
}
} else {
fetch('/api/brand/download/' + taskId, {
method: 'GET',
credentials: 'include',
headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' }),
redirect: 'manual'
})
.then(function(res) {
if (res.status === 301 || res.status === 302) {
var loc = res.headers.get('Location');
if (loc) {
window.open(loc, '_blank');
return;
}
}
var ct = (res.headers && res.headers.get && res.headers.get('content-type')) ? res.headers.get('content-type') : '';
if (ct && ct.indexOf('application/json') >= 0) {
return res.json().then(function(errObj) {
throw new Error((errObj && (errObj.error || errObj.msg)) || '下载失败');
});
}
return res.blob().then(function(blob) {
var url = URL.createObjectURL(blob);
var a2 = document.createElement('a');
a2.href = url;
a2.download = 'brand_task_' + taskId + '.zip';
document.body.appendChild(a2);
a2.click();
a2.remove();
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
});
})
.catch(function(err) {
showToast('下载失败:' + (err && err.message ? err.message : err));
});
}
});
loadTasks();
// setInterval(loadTasks, 10000);
if (window.addEventListener) {
window.addEventListener('pywebviewready', function() {
api = window.pywebview && window.pywebview.api;
});
}
if (window.pywebview && window.pywebview.api) api = window.pywebview.api;
})();
</script>
</body>
</html>