1506 lines
48 KiB
HTML
1506 lines
48 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>小配件闲鱼运营助手</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f6f7f9;
|
||
--panel: #ffffff;
|
||
--text: #1f2933;
|
||
--muted: #677483;
|
||
--line: #dde3ea;
|
||
--primary: #176b87;
|
||
--primary-dark: #0f5369;
|
||
--accent: #d97706;
|
||
--good: #0f8a5f;
|
||
--warn: #b45309;
|
||
--bad: #b42318;
|
||
--soft-good: #e9f7f1;
|
||
--soft-warn: #fff4df;
|
||
--soft-bad: #ffebe8;
|
||
--soft-blue: #e8f3f7;
|
||
--shadow: 0 8px 24px rgba(31, 41, 51, 0.08);
|
||
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", Arial, sans-serif;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
background: #ffffff;
|
||
border-bottom: 1px solid var(--line);
|
||
box-shadow: 0 2px 12px rgba(31, 41, 51, 0.04);
|
||
}
|
||
|
||
.topbar {
|
||
max-width: 1440px;
|
||
margin: 0 auto;
|
||
padding: 14px 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
}
|
||
|
||
.brand {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
min-width: 240px;
|
||
}
|
||
|
||
.brand h1 {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.brand span {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
button,
|
||
.file-label {
|
||
border: 1px solid var(--line);
|
||
background: #ffffff;
|
||
color: var(--text);
|
||
border-radius: 6px;
|
||
padding: 8px 12px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-height: 36px;
|
||
transition: 0.16s ease;
|
||
user-select: none;
|
||
}
|
||
|
||
button:hover,
|
||
.file-label:hover {
|
||
border-color: var(--primary);
|
||
color: var(--primary);
|
||
}
|
||
|
||
button.primary {
|
||
background: var(--primary);
|
||
border-color: var(--primary);
|
||
color: #ffffff;
|
||
}
|
||
|
||
button.primary:hover {
|
||
background: var(--primary-dark);
|
||
color: #ffffff;
|
||
}
|
||
|
||
button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
input[type="file"] {
|
||
display: none;
|
||
}
|
||
|
||
main {
|
||
max-width: 1440px;
|
||
margin: 0 auto;
|
||
padding: 20px 24px 36px;
|
||
display: grid;
|
||
grid-template-columns: 390px minmax(0, 1fr);
|
||
gap: 18px;
|
||
}
|
||
|
||
section,
|
||
aside {
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.input-panel {
|
||
padding: 16px;
|
||
align-self: start;
|
||
position: sticky;
|
||
top: 78px;
|
||
max-height: calc(100vh - 96px);
|
||
overflow: auto;
|
||
}
|
||
|
||
.panel-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.panel-title h2 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.hint {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
margin: 0 0 10px;
|
||
}
|
||
|
||
textarea {
|
||
width: 100%;
|
||
min-height: 280px;
|
||
resize: vertical;
|
||
border: 1px solid var(--line);
|
||
border-radius: 6px;
|
||
padding: 12px;
|
||
font-family: Consolas, "Microsoft YaHei", monospace;
|
||
font-size: 12px;
|
||
line-height: 1.55;
|
||
color: var(--text);
|
||
background: #fbfcfd;
|
||
}
|
||
|
||
textarea:focus,
|
||
input:focus {
|
||
outline: 2px solid rgba(23, 107, 135, 0.16);
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.input-actions {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.input-actions button.primary {
|
||
grid-column: 1 / -1;
|
||
justify-content: center;
|
||
}
|
||
|
||
.settings {
|
||
margin-top: 14px;
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.field {
|
||
display: grid;
|
||
gap: 5px;
|
||
}
|
||
|
||
.field label {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.field input {
|
||
border: 1px solid var(--line);
|
||
border-radius: 6px;
|
||
padding: 8px 10px;
|
||
min-height: 36px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.sample {
|
||
margin-top: 14px;
|
||
border-top: 1px solid var(--line);
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.sample button {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.collector-panel {
|
||
margin-top: 14px;
|
||
border-top: 1px solid var(--line);
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.collector-panel h3 {
|
||
margin: 0 0 10px;
|
||
font-size: 14px;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.collector-actions {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.collector-panel details {
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
background: #fbfcfd;
|
||
}
|
||
|
||
.collector-panel summary {
|
||
cursor: pointer;
|
||
padding: 9px 10px;
|
||
color: var(--text);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.script-box {
|
||
min-height: 160px;
|
||
max-height: 220px;
|
||
border: 0;
|
||
border-top: 1px solid var(--line);
|
||
border-radius: 0 0 8px 8px;
|
||
font-family: Consolas, "Cascadia Mono", monospace;
|
||
font-size: 12px;
|
||
white-space: pre;
|
||
}
|
||
|
||
.content {
|
||
display: grid;
|
||
gap: 16px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.metrics {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, minmax(120px, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.metric {
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
min-height: 82px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.metric span {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.metric strong {
|
||
font-size: 22px;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.metric em {
|
||
color: var(--muted);
|
||
font-style: normal;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 8px;
|
||
padding: 12px 12px 0;
|
||
border-bottom: 1px solid var(--line);
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.tab {
|
||
border: 0;
|
||
border-bottom: 3px solid transparent;
|
||
background: transparent;
|
||
border-radius: 0;
|
||
color: var(--muted);
|
||
white-space: nowrap;
|
||
min-height: 38px;
|
||
}
|
||
|
||
.tab.active {
|
||
color: var(--primary);
|
||
border-bottom-color: var(--primary);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.result-panel {
|
||
min-width: 0;
|
||
}
|
||
|
||
.panel-body {
|
||
padding: 14px;
|
||
}
|
||
|
||
.summary-line {
|
||
background: var(--soft-blue);
|
||
color: var(--primary-dark);
|
||
border: 1px solid #c9e4ed;
|
||
border-radius: 6px;
|
||
padding: 10px 12px;
|
||
margin-bottom: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.table-wrap {
|
||
overflow: auto;
|
||
border: 1px solid var(--line);
|
||
border-radius: 6px;
|
||
background: #ffffff;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
min-width: 900px;
|
||
}
|
||
|
||
th,
|
||
td {
|
||
border-bottom: 1px solid var(--line);
|
||
padding: 9px 10px;
|
||
text-align: left;
|
||
vertical-align: top;
|
||
font-size: 13px;
|
||
}
|
||
|
||
th {
|
||
background: #f8fafc;
|
||
color: #425466;
|
||
font-weight: 700;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
}
|
||
|
||
tr:last-child td {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.title-cell {
|
||
max-width: 340px;
|
||
min-width: 260px;
|
||
}
|
||
|
||
.muted {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 28px;
|
||
height: 22px;
|
||
border-radius: 999px;
|
||
padding: 0 8px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.badge.s {
|
||
background: var(--soft-good);
|
||
color: var(--good);
|
||
}
|
||
|
||
.badge.a {
|
||
background: var(--soft-blue);
|
||
color: var(--primary);
|
||
}
|
||
|
||
.badge.b {
|
||
background: var(--soft-warn);
|
||
color: var(--warn);
|
||
}
|
||
|
||
.badge.c {
|
||
background: #eef2f7;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.badge.risk {
|
||
background: var(--soft-bad);
|
||
color: var(--bad);
|
||
}
|
||
|
||
.cards-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.note-card {
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.note-card h3 {
|
||
margin: 0 0 8px;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.note-card p {
|
||
margin: 0;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.markdown-box {
|
||
width: 100%;
|
||
min-height: 460px;
|
||
white-space: pre;
|
||
}
|
||
|
||
.empty {
|
||
padding: 28px;
|
||
text-align: center;
|
||
color: var(--muted);
|
||
border: 1px dashed var(--line);
|
||
border-radius: 8px;
|
||
background: #fbfcfd;
|
||
}
|
||
|
||
.toast {
|
||
position: fixed;
|
||
right: 24px;
|
||
bottom: 24px;
|
||
background: #1f2933;
|
||
color: #ffffff;
|
||
padding: 10px 14px;
|
||
border-radius: 6px;
|
||
box-shadow: var(--shadow);
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transform: translateY(8px);
|
||
transition: 0.18s ease;
|
||
z-index: 30;
|
||
}
|
||
|
||
.toast.show {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
@media (max-width: 980px) {
|
||
main {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.input-panel {
|
||
position: static;
|
||
max-height: none;
|
||
}
|
||
|
||
.metrics {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.cards-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.topbar {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.toolbar {
|
||
width: 100%;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
main {
|
||
padding: 14px;
|
||
}
|
||
|
||
.metrics {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="topbar">
|
||
<div class="brand">
|
||
<h1>小配件闲鱼运营助手</h1>
|
||
<span>粘贴商品数据,自动拆出补品、优化、下架和周报</span>
|
||
</div>
|
||
<div class="toolbar">
|
||
<button id="copyMarkdownBtn" type="button" title="复制周报 Markdown">复制周报</button>
|
||
<button id="downloadMarkdownBtn" type="button" title="下载周报 Markdown">下载周报</button>
|
||
<button id="resetBtn" type="button" title="清空输入和分析结果">清空</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<aside class="input-panel">
|
||
<div class="panel-title">
|
||
<h2>数据输入</h2>
|
||
</div>
|
||
<p class="hint">从闲鱼商品数据页复制表格后粘贴进来。第一版支持制表符、CSV、空格分隔文本和包含“商品ID”的行。</p>
|
||
<textarea id="dataInput" placeholder="粘贴商品数据,例如:
|
||
商品信息 商品曝光人数 商品浏览人数 询单人数 支付人数 支付订单数 支付金额 成功退款订单数 成功退款金额
|
||
小天才D2A/Q2A表带 商品ID 953520780202 ¥6.80 5317 219 84 71 74 607 0 0"></textarea>
|
||
|
||
<div class="input-actions">
|
||
<label class="file-label" for="fileInput">上传 CSV/TXT</label>
|
||
<input id="fileInput" type="file" accept=".csv,.txt,.tsv">
|
||
<button id="loadSampleBtn" type="button">载入示例</button>
|
||
<button id="analyzeBtn" class="primary" type="button">分析数据</button>
|
||
</div>
|
||
|
||
<div class="settings">
|
||
<div class="field">
|
||
<label for="dateRange">数据范围</label>
|
||
<input id="dateRange" value="2026-05-30 ~ 2026-06-28">
|
||
</div>
|
||
<div class="field">
|
||
<label for="shopMode">运营模式</label>
|
||
<input id="shopMode" value="拼多多代下单,单单利润约 2~5 元">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sample">
|
||
<p class="hint">建议每周固定复制近 7 天或近 30 天商品数据,导出 Markdown 存到 Obsidian。</p>
|
||
</div>
|
||
|
||
<div class="collector-panel">
|
||
<h3>浏览器采集</h3>
|
||
<p class="hint">打开闲鱼商品数据页后,把下面脚本粘到浏览器控制台执行;脚本会自动滚动当前页并尝试翻页采集,结果复制到剪贴板后回到这里粘贴分析。</p>
|
||
<div class="collector-actions">
|
||
<button id="copyCollectorBtn" type="button">复制采集脚本</button>
|
||
<button id="pasteClipboardBtn" type="button">粘贴剪贴板</button>
|
||
</div>
|
||
<details>
|
||
<summary>查看采集脚本</summary>
|
||
<textarea id="collectorScript" class="script-box" readonly></textarea>
|
||
</details>
|
||
</div>
|
||
</aside>
|
||
|
||
<div class="content">
|
||
<div class="metrics">
|
||
<div class="metric">
|
||
<span>分析商品</span>
|
||
<strong id="metricItems">0</strong>
|
||
<em>按商品 ID 去重</em>
|
||
</div>
|
||
<div class="metric">
|
||
<span>支付订单</span>
|
||
<strong id="metricOrders">0</strong>
|
||
<em>近周期合计</em>
|
||
</div>
|
||
<div class="metric">
|
||
<span>支付金额</span>
|
||
<strong id="metricAmount">0</strong>
|
||
<em>元</em>
|
||
</div>
|
||
<div class="metric">
|
||
<span>退款金额</span>
|
||
<strong id="metricRefund">0</strong>
|
||
<em>元</em>
|
||
</div>
|
||
<div class="metric">
|
||
<span>主力类目</span>
|
||
<strong id="metricCategory">-</strong>
|
||
<em>按支付订单判断</em>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="result-panel">
|
||
<div class="tabs" role="tablist">
|
||
<button class="tab active" data-tab="supplement" type="button">补品清单</button>
|
||
<button class="tab" data-tab="optimize" type="button">优化清单</button>
|
||
<button class="tab" data-tab="remove" type="button">下架清单</button>
|
||
<button class="tab" data-tab="review" type="button">本周复盘</button>
|
||
<button class="tab" data-tab="data" type="button">解析数据</button>
|
||
</div>
|
||
<div id="tabBody" class="panel-body">
|
||
<div class="empty">粘贴数据后点击“分析数据”,这里会生成三张清单和周报。</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
|
||
<div id="toast" class="toast">已复制</div>
|
||
|
||
<script>
|
||
const state = {
|
||
activeTab: "supplement",
|
||
rows: [],
|
||
results: {
|
||
supplement: [],
|
||
optimize: [],
|
||
remove: [],
|
||
markdown: "",
|
||
categoryStats: [],
|
||
},
|
||
};
|
||
|
||
const sampleText = `商品信息\t商品曝光人数\t商品浏览人数\t询单人数\t支付人数\t支付订单数\t支付金额\t成功退款订单数\t成功退款金额
|
||
小天才电话手表表带,适用D2A/Q2A型号,全新未使用。黑色 商品ID 953520780202 ¥6.80\t5317\t219\t84\t71\t74\t607\t0\t0
|
||
小天才D3电话手表表带,适用W2028AC机型。蓝色和红色两 商品ID 973482389112 ¥6.80\t3287\t133\t49\t40\t41\t309.6\t0\t0
|
||
【全新正品】适用小天才电话手表Z6Pro保护壳,渐变色软胶全 商品ID 978206766501 ¥8.80\t1262\t52\t6\t4\t5\t51.6\t0\t0
|
||
【全新】适用小天才Z7/Z7A/Z7S儿童电话手表表带,晴海 商品ID 988230832539 ¥6.80\t826\t22\t7\t4\t4\t28.3\t0\t0
|
||
个人闲置小米手环10腕带,编织款,女生戴很有高级感 商品ID 1063128096716 ¥20.90\t940\t116\t1\t0\t0\t0\t0\t0
|
||
个人闲置,适用小天才电话手表Z11保护壳,卡通立体公仔款 商品ID 1002451280948 ¥7.80\t226\t4\t0\t0\t0\t0\t0\t0
|
||
个人闲置三个小厨电动刮鱼鳞神器,无线充电款 商品ID 1055460443702 ¥55.00\t382\t15\t1\t1\t1\t47.8\t1\t47.8
|
||
个人闲置含羞草大盆栽,绿植,室内阳台都能养 商品ID 1042983596529 ¥10.90\t327\t14\t1\t0\t0\t0\t0\t0
|
||
仿真三蹦子三轮车,特价处理!蓝色和红色可选 商品ID 993119465849 ¥13.50\t2279\t15\t0\t0\t0\t0\t0\t0`;
|
||
|
||
const els = {
|
||
input: document.getElementById("dataInput"),
|
||
file: document.getElementById("fileInput"),
|
||
analyze: document.getElementById("analyzeBtn"),
|
||
sample: document.getElementById("loadSampleBtn"),
|
||
reset: document.getElementById("resetBtn"),
|
||
copy: document.getElementById("copyMarkdownBtn"),
|
||
download: document.getElementById("downloadMarkdownBtn"),
|
||
copyCollector: document.getElementById("copyCollectorBtn"),
|
||
pasteClipboard: document.getElementById("pasteClipboardBtn"),
|
||
collectorScript: document.getElementById("collectorScript"),
|
||
tabBody: document.getElementById("tabBody"),
|
||
toast: document.getElementById("toast"),
|
||
dateRange: document.getElementById("dateRange"),
|
||
shopMode: document.getElementById("shopMode"),
|
||
metricItems: document.getElementById("metricItems"),
|
||
metricOrders: document.getElementById("metricOrders"),
|
||
metricAmount: document.getElementById("metricAmount"),
|
||
metricRefund: document.getElementById("metricRefund"),
|
||
metricCategory: document.getElementById("metricCategory"),
|
||
};
|
||
|
||
document.querySelectorAll(".tab").forEach((tab) => {
|
||
tab.addEventListener("click", () => {
|
||
state.activeTab = tab.dataset.tab;
|
||
document.querySelectorAll(".tab").forEach((x) => x.classList.toggle("active", x === tab));
|
||
renderTab();
|
||
});
|
||
});
|
||
|
||
els.sample.addEventListener("click", () => {
|
||
els.input.value = sampleText;
|
||
showToast("已载入示例数据");
|
||
});
|
||
|
||
els.analyze.addEventListener("click", () => {
|
||
const rows = parseInput(els.input.value);
|
||
if (!rows.length) {
|
||
showToast("没有识别到商品数据");
|
||
return;
|
||
}
|
||
state.rows = rows;
|
||
state.results = analyzeRows(rows);
|
||
updateMetrics();
|
||
renderTab();
|
||
showToast(`已分析 ${rows.length} 个商品`);
|
||
});
|
||
|
||
els.reset.addEventListener("click", () => {
|
||
els.input.value = "";
|
||
state.rows = [];
|
||
state.results = { supplement: [], optimize: [], remove: [], markdown: "", categoryStats: [] };
|
||
updateMetrics();
|
||
renderEmpty();
|
||
showToast("已清空");
|
||
});
|
||
|
||
els.copy.addEventListener("click", async () => {
|
||
if (!state.results.markdown) {
|
||
showToast("暂无周报可复制");
|
||
return;
|
||
}
|
||
await navigator.clipboard.writeText(state.results.markdown);
|
||
showToast("周报已复制");
|
||
});
|
||
|
||
els.download.addEventListener("click", () => {
|
||
if (!state.results.markdown) {
|
||
showToast("暂无周报可下载");
|
||
return;
|
||
}
|
||
const blob = new Blob([state.results.markdown], { type: "text/markdown;charset=utf-8" });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
const safeDate = els.dateRange.value.replace(/[^\d]+/g, "-").replace(/^-|-$/g, "");
|
||
a.href = url;
|
||
a.download = `小配件闲鱼运营周报_${safeDate || "未命名"}.md`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
});
|
||
|
||
els.file.addEventListener("change", async (event) => {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
const text = await file.text();
|
||
els.input.value = text;
|
||
showToast(`已读取 ${file.name}`);
|
||
});
|
||
|
||
els.copyCollector.addEventListener("click", async () => {
|
||
const script = getCollectorScript();
|
||
els.collectorScript.value = script;
|
||
const copied = await writeClipboard(script);
|
||
if (!copied) {
|
||
els.collectorScript.focus();
|
||
els.collectorScript.select();
|
||
}
|
||
showToast(copied ? "采集脚本已复制" : "已选中脚本,请手动复制");
|
||
});
|
||
|
||
els.pasteClipboard.addEventListener("click", async () => {
|
||
try {
|
||
const text = await navigator.clipboard.readText();
|
||
if (!text.trim()) {
|
||
showToast("剪贴板为空");
|
||
return;
|
||
}
|
||
els.input.value = text;
|
||
showToast("已粘贴剪贴板数据");
|
||
} catch (error) {
|
||
els.input.focus();
|
||
showToast("请手动粘贴到输入框");
|
||
}
|
||
});
|
||
|
||
async function writeClipboard(text) {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
return true;
|
||
} catch (error) {
|
||
const helper = document.createElement("textarea");
|
||
helper.value = text;
|
||
helper.setAttribute("readonly", "readonly");
|
||
helper.style.position = "fixed";
|
||
helper.style.left = "-9999px";
|
||
document.body.appendChild(helper);
|
||
helper.select();
|
||
const ok = document.execCommand("copy");
|
||
document.body.removeChild(helper);
|
||
return ok;
|
||
}
|
||
}
|
||
|
||
function getCollectorScript() {
|
||
return `(async () => {
|
||
const header = ["商品信息", "商品曝光人数", "商品浏览人数", "询单人数", "支付人数", "支付订单数", "支付金额", "成功退款订单数", "成功退款金额"];
|
||
const config = {
|
||
maxPages: 12,
|
||
maxScrollRounds: 18,
|
||
scrollDelay: 450,
|
||
pageDelay: 1200
|
||
};
|
||
const clean = (value) => String(value || "").replace(/\\s+/g, " ").trim();
|
||
const rowSelectors = [
|
||
".ant-table-tbody tr",
|
||
".next-table-body tr",
|
||
"table tbody tr",
|
||
"[role='row']"
|
||
];
|
||
const seen = new Set();
|
||
const outputRows = [];
|
||
let pageCount = 1;
|
||
|
||
function cellsOf(row) {
|
||
const cellNodes = row.querySelectorAll("td, th, [role='gridcell'], [role='cell']");
|
||
let cells = Array.from(cellNodes).map((cell) => clean(cell.innerText)).filter(Boolean);
|
||
if (!cells.length) cells = clean(row.innerText).split(/\\n+/).map(clean).filter(Boolean);
|
||
return cells;
|
||
}
|
||
|
||
function findTitleIndex(cells) {
|
||
const index = cells.findIndex((cell) => /商品ID\\s*\\d+|\\b\\d{10,}\\b/.test(cell));
|
||
return index >= 0 ? index : 0;
|
||
}
|
||
|
||
function normalizeRow(cells) {
|
||
if (!cells.length) return null;
|
||
const titleIndex = findTitleIndex(cells);
|
||
const title = clean(cells[titleIndex]);
|
||
if (!/商品ID\\s*\\d+|\\b\\d{10,}\\b/.test(title)) return null;
|
||
const nums = cells
|
||
.slice(titleIndex + 1)
|
||
.flatMap((cell) => (cell.match(/-?\\d+(?:\\.\\d+)?/g) || []))
|
||
.map(Number)
|
||
.filter(Number.isFinite);
|
||
if (nums.length < 3) return null;
|
||
while (nums.length < 8) nums.push(0);
|
||
return [title, ...nums.slice(0, 8)].join("\\t");
|
||
}
|
||
|
||
function pushRow(line) {
|
||
if (!line || seen.has(line)) return;
|
||
seen.add(line);
|
||
outputRows.push(line);
|
||
}
|
||
|
||
function collectVisibleRows() {
|
||
let before = outputRows.length;
|
||
for (const selector of rowSelectors) {
|
||
document.querySelectorAll(selector).forEach((row) => pushRow(normalizeRow(cellsOf(row))));
|
||
}
|
||
return outputRows.length - before;
|
||
}
|
||
|
||
function getScrollTargets() {
|
||
const nodes = Array.from(document.querySelectorAll("main, section, div, table, tbody"));
|
||
const targets = nodes
|
||
.filter((node) => {
|
||
const style = getComputedStyle(node);
|
||
const overflowY = style.overflowY;
|
||
return node.scrollHeight > node.clientHeight + 80 && /(auto|scroll|overlay)/.test(overflowY);
|
||
})
|
||
.sort((a, b) => (b.scrollHeight - b.clientHeight) - (a.scrollHeight - a.clientHeight));
|
||
targets.push(document.scrollingElement || document.documentElement);
|
||
return Array.from(new Set(targets));
|
||
}
|
||
|
||
function sleep(ms) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
|
||
async function scrollAndCollectPage() {
|
||
let stableRounds = 0;
|
||
let lastCount = outputRows.length;
|
||
const targets = getScrollTargets();
|
||
|
||
collectVisibleRows();
|
||
for (let round = 0; round < config.maxScrollRounds; round += 1) {
|
||
for (const target of targets) {
|
||
target.scrollTop = Math.min(target.scrollTop + Math.max(420, target.clientHeight || 600), target.scrollHeight);
|
||
}
|
||
window.scrollBy(0, Math.max(480, window.innerHeight * 0.8));
|
||
await sleep(config.scrollDelay);
|
||
collectVisibleRows();
|
||
|
||
if (outputRows.length === lastCount) stableRounds += 1;
|
||
else stableRounds = 0;
|
||
lastCount = outputRows.length;
|
||
if (stableRounds >= 3) break;
|
||
}
|
||
}
|
||
|
||
function isDisabled(element) {
|
||
if (!element) return true;
|
||
const text = clean(element.innerText || element.getAttribute("aria-label") || element.title || "");
|
||
const className = String(element.className || "");
|
||
return element.disabled ||
|
||
element.getAttribute("aria-disabled") === "true" ||
|
||
/disabled|next-disabled|btn-disabled/.test(className) ||
|
||
/没有下一页|最后一页/.test(text);
|
||
}
|
||
|
||
function findNextButton() {
|
||
const candidates = Array.from(document.querySelectorAll("button, a, li, span, div"))
|
||
.filter((node) => {
|
||
const text = clean(node.innerText || node.getAttribute("aria-label") || node.title || "");
|
||
const className = String(node.className || "");
|
||
if (!text && !className) return false;
|
||
const looksNext = /下一页|下页|Next|next|right|arrow-right|chevron-right/i.test(text + " " + className);
|
||
const rect = node.getBoundingClientRect();
|
||
return looksNext && rect.width > 0 && rect.height > 0 && !isDisabled(node);
|
||
});
|
||
return candidates.find((node) => node.closest("ul, nav, .pagination, [class*='pagination'], [class*='pager']")) || candidates[0] || null;
|
||
}
|
||
|
||
function pageFingerprint() {
|
||
const rows = [];
|
||
for (const selector of rowSelectors) {
|
||
document.querySelectorAll(selector).forEach((row) => rows.push(clean(row.innerText)));
|
||
}
|
||
return location.href + "||" + rows.slice(0, 20).join("||").slice(0, 2400);
|
||
}
|
||
|
||
async function goNextPage() {
|
||
const before = pageFingerprint();
|
||
const button = findNextButton();
|
||
if (!button) return false;
|
||
button.click();
|
||
await sleep(config.pageDelay);
|
||
const after = pageFingerprint();
|
||
return before !== after;
|
||
}
|
||
|
||
const status = document.createElement("div");
|
||
status.style.cssText = "position:fixed;right:16px;bottom:16px;z-index:2147483647;background:#1f2933;color:#fff;padding:10px 12px;border-radius:6px;font:13px sans-serif;box-shadow:0 8px 24px rgba(0,0,0,.18)";
|
||
document.body.appendChild(status);
|
||
|
||
for (let page = 1; page <= config.maxPages; page += 1) {
|
||
pageCount = page;
|
||
status.textContent = "正在采集第 " + page + " 页,已采集 " + outputRows.length + " 行...";
|
||
await scrollAndCollectPage();
|
||
status.textContent = "第 " + page + " 页采集完成,累计 " + outputRows.length + " 行。";
|
||
const moved = await goNextPage();
|
||
if (!moved) break;
|
||
}
|
||
|
||
status.remove();
|
||
const text = [header.join("\\t"), ...outputRows].join("\\n");
|
||
if (!outputRows.length) {
|
||
alert("没有采集到商品数据,请确认当前页是闲鱼商品数据表格,或先滚动/翻页加载列表。");
|
||
return;
|
||
}
|
||
|
||
navigator.clipboard.writeText(text)
|
||
.then(() => alert("已采集 " + outputRows.length + " 行商品数据,覆盖约 " + pageCount + " 页,并复制到剪贴板。"))
|
||
.catch(() => {
|
||
const box = document.createElement("textarea");
|
||
box.value = text;
|
||
box.style.position = "fixed";
|
||
box.style.left = "12px";
|
||
box.style.top = "12px";
|
||
box.style.width = "520px";
|
||
box.style.height = "260px";
|
||
box.style.zIndex = "2147483647";
|
||
document.body.appendChild(box);
|
||
box.focus();
|
||
box.select();
|
||
alert("已采集 " + outputRows.length + " 行,复制权限被浏览器拦截。请手动复制左上角文本框。");
|
||
});
|
||
})();`;
|
||
}
|
||
|
||
function parseInput(rawText) {
|
||
const lines = rawText
|
||
.replace(/\r/g, "")
|
||
.split("\n")
|
||
.map((line) => line.trim())
|
||
.filter(Boolean);
|
||
if (!lines.length) return [];
|
||
|
||
const dataLines = lines.filter((line) => /商品ID\s*\d+/.test(line) || /\t/.test(line));
|
||
const rows = [];
|
||
for (const line of dataLines) {
|
||
if (!/商品ID\s*\d+/.test(line)) continue;
|
||
rows.push(parseLine(line));
|
||
}
|
||
|
||
const map = new Map();
|
||
rows.filter(Boolean).forEach((row) => {
|
||
if (!map.has(row.itemId)) map.set(row.itemId, row);
|
||
});
|
||
return Array.from(map.values()).map(enrichRow);
|
||
}
|
||
|
||
function parseLine(line) {
|
||
const itemId = matchFirst(line, /商品ID\s*(\d+)/);
|
||
if (!itemId) return null;
|
||
|
||
const title = cleanTitle(line);
|
||
const price = numberFrom(matchFirst(line, /¥\s*([\d.]+)/));
|
||
const parts = line.includes("\t") ? line.split("\t").map((x) => x.trim()).filter(Boolean) : [];
|
||
let nums = [];
|
||
|
||
if (parts.length >= 8) {
|
||
nums = parts.slice(1).map(numberFrom).filter((n) => Number.isFinite(n));
|
||
} else {
|
||
const tail = line.replace(/^.*?商品ID\s*\d+/, "");
|
||
nums = (tail.match(/-?\d+(?:\.\d+)?%?/g) || [])
|
||
.map((x) => x.includes("%") ? null : numberFrom(x))
|
||
.filter((n) => Number.isFinite(n));
|
||
if (nums.length && Math.abs(nums[0] - price) < 0.001) nums.shift();
|
||
}
|
||
|
||
return {
|
||
itemId,
|
||
title,
|
||
price,
|
||
exposurePeople: nums[0] || 0,
|
||
viewPeople: nums[1] || 0,
|
||
inquiryPeople: nums[2] || 0,
|
||
payPeople: nums[3] || 0,
|
||
payOrders: nums[4] || 0,
|
||
payAmount: nums[5] || 0,
|
||
successRefundOrders: nums[6] || 0,
|
||
successRefundAmount: nums[7] || 0,
|
||
};
|
||
}
|
||
|
||
function cleanTitle(line) {
|
||
return line
|
||
.replace(/\t/g, " ")
|
||
.replace(/商品ID\s*\d+.*$/, "")
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
}
|
||
|
||
function matchFirst(text, regex) {
|
||
const m = text.match(regex);
|
||
return m ? m[1] : "";
|
||
}
|
||
|
||
function numberFrom(value) {
|
||
if (value === undefined || value === null || value === "" || value === "-") return 0;
|
||
const n = Number(String(value).replace(/[,%¥]/g, "").trim());
|
||
return Number.isFinite(n) ? n : 0;
|
||
}
|
||
|
||
function enrichRow(row) {
|
||
const category = classifyCategory(row.title);
|
||
const model = detectModel(row.title);
|
||
const accessoryType = detectAccessoryType(row.title);
|
||
const viewRate = row.exposurePeople ? row.viewPeople / row.exposurePeople : 0;
|
||
const payRate = row.viewPeople ? row.payPeople / row.viewPeople : 0;
|
||
const refundRate = row.payOrders ? row.successRefundOrders / row.payOrders : 0;
|
||
return { ...row, category, model, accessoryType, viewRate, payRate, refundRate };
|
||
}
|
||
|
||
function classifyCategory(title) {
|
||
if (/小天才|米兔|电话手表|手环|腕带|表带|保护壳|充电线|螺丝杆|底托|卡盖|卡托|Z\d|D\d|Q\d|Apple|苹果/i.test(title)) return "手表/手环配件";
|
||
if (/拔毛|刮鱼鳞|菜板|厨房|切菜|蟹|杀鸡|鸡鸭鹅|家禽|封口机|锅|猫砂铲|烧烤架/i.test(title)) return "厨房/家居工具";
|
||
if (/马扎|折叠凳|钓鱼|户外|自行车|气嘴灯|后视镜|露营|赶海|筛铲/i.test(title)) return "户外/骑行小物";
|
||
if (/儿童|玩具|捞鱼|象棋|积木|教具|文具|算盘|华容道|叠叠杯|宝宝|早教|学习桌|跳棋|故事书/i.test(title)) return "儿童玩具/教具";
|
||
if (/蓝莓|树苗|含羞草|盆栽|绿植|薄荷|浇水/i.test(title)) return "绿植盆栽";
|
||
return "其他";
|
||
}
|
||
|
||
function detectModel(title) {
|
||
const upper = title.toUpperCase();
|
||
const knownCombos = [
|
||
"D2A/Q2A", "Q1A/Q2A/D2/D2A", "Z7/Z7A/Z7S", "Z6/Z6A/Z6S/Z6PRO",
|
||
"Z6A/Z6S", "S10/S11", "9PRO/8PRO"
|
||
];
|
||
for (const combo of knownCombos) {
|
||
if (upper.includes(combo)) return combo;
|
||
}
|
||
const matches = upper.match(/Z\d+[A-Z]*|D\d+[A-Z]*|Q\d+[A-Z]*|\d+X|5C|B4|S\d+/g);
|
||
return matches ? Array.from(new Set(matches)).slice(0, 3).join("/") : "";
|
||
}
|
||
|
||
function detectAccessoryType(title) {
|
||
if (/充电线|充电器|磁吸充电/i.test(title)) return "充电线";
|
||
if (/保护壳|保护套|表壳|壳膜/i.test(title)) return "保护壳";
|
||
if (/表带|腕带|表链/i.test(title)) return "表带";
|
||
if (/螺丝|螺丝杆|螺丝刀/i.test(title)) return "螺丝杆";
|
||
if (/底托|托盘|旋转配件/i.test(title)) return "底托";
|
||
if (/卡盖|卡托/i.test(title)) return "卡盖/卡托";
|
||
if (/贴膜|钢化膜|保护膜/i.test(title)) return "贴膜";
|
||
return "其他配件";
|
||
}
|
||
|
||
function analyzeRows(rows) {
|
||
const categoryStats = buildCategoryStats(rows);
|
||
const supplement = buildSupplementList(rows);
|
||
const optimize = buildOptimizeList(rows);
|
||
const remove = buildRemoveList(rows);
|
||
const markdown = buildMarkdown(rows, categoryStats, supplement, optimize, remove);
|
||
return { supplement, optimize, remove, markdown, categoryStats };
|
||
}
|
||
|
||
function buildCategoryStats(rows) {
|
||
const stats = {};
|
||
rows.forEach((row) => {
|
||
const item = stats[row.category] || {
|
||
category: row.category,
|
||
items: 0,
|
||
exposurePeople: 0,
|
||
viewPeople: 0,
|
||
inquiryPeople: 0,
|
||
payOrders: 0,
|
||
payAmount: 0,
|
||
refundOrders: 0,
|
||
refundAmount: 0,
|
||
};
|
||
item.items += 1;
|
||
item.exposurePeople += row.exposurePeople;
|
||
item.viewPeople += row.viewPeople;
|
||
item.inquiryPeople += row.inquiryPeople;
|
||
item.payOrders += row.payOrders;
|
||
item.payAmount += row.payAmount;
|
||
item.refundOrders += row.successRefundOrders;
|
||
item.refundAmount += row.successRefundAmount;
|
||
stats[row.category] = item;
|
||
});
|
||
return Object.values(stats)
|
||
.map((item) => ({
|
||
...item,
|
||
payRate: item.viewPeople ? item.payOrders / item.viewPeople : 0,
|
||
}))
|
||
.sort((a, b) => b.payOrders - a.payOrders || b.payAmount - a.payAmount);
|
||
}
|
||
|
||
function buildSupplementList(rows) {
|
||
const candidates = rows
|
||
.filter((row) => row.payOrders >= 3 && row.successRefundOrders === 0 && isSmallAccessory(row))
|
||
.map((row) => {
|
||
const priority = row.payOrders >= 20 ? "S" : row.payOrders >= 5 ? "A" : "B";
|
||
return {
|
||
...row,
|
||
priority,
|
||
reason: `${row.model || row.accessoryType} 近周期 ${row.payOrders} 单,退款 0`,
|
||
suggestion: supplementSuggestion(row),
|
||
};
|
||
})
|
||
.sort(prioritySort);
|
||
|
||
return candidates;
|
||
}
|
||
|
||
function buildOptimizeList(rows) {
|
||
return rows
|
||
.filter((row) => {
|
||
const hasTrafficNoPay = row.exposurePeople >= 200 && row.viewPeople > 0 && row.payOrders === 0;
|
||
const hasInquiryNoPay = row.inquiryPeople > 0 && row.payOrders === 0;
|
||
return hasTrafficNoPay || hasInquiryNoPay;
|
||
})
|
||
.map((row) => ({
|
||
...row,
|
||
priority: row.inquiryPeople >= 3 || row.viewPeople >= 50 ? "A" : "B",
|
||
reason: optimizeReason(row),
|
||
suggestion: optimizeSuggestion(row),
|
||
}))
|
||
.sort((a, b) => b.viewPeople - a.viewPeople || b.exposurePeople - a.exposurePeople);
|
||
}
|
||
|
||
function buildRemoveList(rows) {
|
||
return rows
|
||
.filter((row) => {
|
||
const refundBad = row.payOrders > 0 && row.successRefundOrders / row.payOrders >= 0.4;
|
||
const categoryRisk = ["厨房/家居工具", "绿植盆栽", "其他"].includes(row.category) && row.payOrders === 0 && row.exposurePeople >= 150;
|
||
const noClick = row.exposurePeople >= 200 && row.viewPeople === 0;
|
||
const highExposeLowClick = row.exposurePeople >= 800 && row.viewRate < 0.012 && row.payOrders === 0;
|
||
return refundBad || categoryRisk || noClick || highExposeLowClick;
|
||
})
|
||
.map((row) => ({
|
||
...row,
|
||
priority: row.successRefundOrders > 0 || row.exposurePeople >= 800 ? "S" : "A",
|
||
reason: removeReason(row),
|
||
suggestion: removeSuggestion(row),
|
||
}))
|
||
.sort(prioritySort);
|
||
}
|
||
|
||
function isSmallAccessory(row) {
|
||
if (row.category !== "手表/手环配件") return false;
|
||
if (!row.model && !/小天才|米兔|手环|电话手表/i.test(row.title)) return false;
|
||
return row.price <= 20 || row.payOrders >= 3;
|
||
}
|
||
|
||
function supplementSuggestion(row) {
|
||
const model = row.model || "同型号";
|
||
if (row.accessoryType === "表带") return `补充 ${model} 充电线、保护壳、卡盖、螺丝杆。`;
|
||
if (row.accessoryType === "保护壳") return `补充 ${model} 表带、充电线、贴膜。`;
|
||
if (row.accessoryType === "充电线") return `补充 ${model} 表带、保护壳。`;
|
||
if (row.accessoryType.includes("卡盖") || row.accessoryType.includes("螺丝")) return `补充 ${model} 维修小配件组合。`;
|
||
return `围绕 ${model} 补充表带、保护壳、充电线。`;
|
||
}
|
||
|
||
function optimizeReason(row) {
|
||
if (row.inquiryPeople > 0) return `有 ${row.inquiryPeople} 个询单但成交 0。`;
|
||
if (row.viewPeople >= 50) return `浏览 ${row.viewPeople},成交 0。`;
|
||
return `曝光 ${row.exposurePeople},浏览 ${row.viewPeople},成交 0。`;
|
||
}
|
||
|
||
function optimizeSuggestion(row) {
|
||
if (row.viewPeople >= 50 && row.payOrders === 0) return "先降价或换主图,补清楚适配型号,不扩同类新链接。";
|
||
if (row.inquiryPeople > 0 && row.payOrders === 0) return "检查价格、发货时效和规格说明,整理买家疑虑。";
|
||
if (row.viewRate < 0.015) return "换首图,标题前 20 字放型号和配件类型。";
|
||
return "优化标题、主图和详情,观察 7 天。";
|
||
}
|
||
|
||
function removeReason(row) {
|
||
if (row.successRefundOrders > 0) return `退款 ${row.successRefundOrders} 单 / ${money(row.successRefundAmount)} 元。`;
|
||
if (row.viewPeople === 0) return `曝光 ${row.exposurePeople} 但无浏览。`;
|
||
if (row.viewRate < 0.012) return `曝光 ${row.exposurePeople},点击率过低。`;
|
||
return `${row.category} 非主线且无成交。`;
|
||
}
|
||
|
||
function removeSuggestion(row) {
|
||
if (row.successRefundOrders > 0) return "停止扩品,复查货源质量和售后,必要时下架。";
|
||
if (row.category === "绿植盆栽") return "不继续维护,优先下架或停止上新。";
|
||
if (row.category === "厨房/家居工具") return "售后风险高,不再新增,低效链接逐步清理。";
|
||
if (row.viewPeople === 0 || row.viewRate < 0.012) return "下架或重做主图标题后只观察 7 天。";
|
||
return "与主线不一致,优先清理货盘。";
|
||
}
|
||
|
||
function prioritySort(a, b) {
|
||
const weight = { S: 3, A: 2, B: 1, C: 0 };
|
||
return (weight[b.priority] || 0) - (weight[a.priority] || 0) || b.payOrders - a.payOrders || b.payAmount - a.payAmount;
|
||
}
|
||
|
||
function buildMarkdown(rows, categoryStats, supplement, optimize, remove) {
|
||
const totals = getTotals(rows);
|
||
const mainCategory = getMainCategory(categoryStats);
|
||
const dateRange = els.dateRange.value || "未填写";
|
||
const lines = [];
|
||
lines.push("# 小配件闲鱼运营助手周报");
|
||
lines.push("");
|
||
lines.push(`数据范围:\`${dateRange}\``);
|
||
lines.push("");
|
||
lines.push("## 总览");
|
||
lines.push("");
|
||
lines.push("| 指标 | 数值 |");
|
||
lines.push("|---|---:|");
|
||
lines.push(`| 分析商品 | ${rows.length} 个 |`);
|
||
lines.push(`| 支付订单 | ${totals.payOrders} 单 |`);
|
||
lines.push(`| 支付金额 | ${money(totals.payAmount)} 元 |`);
|
||
lines.push(`| 退款订单 | ${totals.refundOrders} 单 |`);
|
||
lines.push(`| 退款金额 | ${money(totals.refundAmount)} 元 |`);
|
||
lines.push(`| 主力类目 | ${mainCategory || "-"} |`);
|
||
lines.push("");
|
||
lines.push(`核心结论:${mainCategory || "当前数据不足"} 是当前最值得优先观察的方向。`);
|
||
lines.push("");
|
||
lines.push("## 类目表现");
|
||
lines.push("");
|
||
lines.push("| 类目 | 商品数 | 支付订单 | 支付金额 | 退款金额 | 浏览支付率 |");
|
||
lines.push("|---|---:|---:|---:|---:|---:|");
|
||
categoryStats.forEach((item) => {
|
||
lines.push(`| ${item.category} | ${item.items} | ${item.payOrders} | ${money(item.payAmount)} | ${money(item.refundAmount)} | ${percent(item.payRate)} |`);
|
||
});
|
||
lines.push("");
|
||
lines.push("## 补品清单");
|
||
lines.push("");
|
||
pushResultTable(lines, supplement);
|
||
lines.push("");
|
||
lines.push("## 优化清单");
|
||
lines.push("");
|
||
pushResultTable(lines, optimize);
|
||
lines.push("");
|
||
lines.push("## 下架清单");
|
||
lines.push("");
|
||
pushResultTable(lines, remove);
|
||
lines.push("");
|
||
lines.push("## 下周动作");
|
||
lines.push("");
|
||
const actions = buildNextActions(supplement, remove);
|
||
actions.forEach((action, index) => lines.push(`${index + 1}. ${action}`));
|
||
lines.push("");
|
||
lines.push("## 一句话建议");
|
||
lines.push("");
|
||
lines.push(buildOneLineAdvice(categoryStats, supplement));
|
||
lines.push("");
|
||
return lines.join("\n");
|
||
}
|
||
|
||
function pushResultTable(lines, rows) {
|
||
if (!rows.length) {
|
||
lines.push("暂无。");
|
||
return;
|
||
}
|
||
lines.push("| 优先级 | 商品 | 数据 | 建议 |");
|
||
lines.push("|---|---|---:|---|");
|
||
rows.slice(0, 20).forEach((row) => {
|
||
const data = `${row.payOrders}单 / ${money(row.payAmount)}元 / 退款${row.successRefundOrders}`;
|
||
lines.push(`| ${row.priority} | ${escapeMd(row.title)} | ${data} | ${escapeMd(row.suggestion)} |`);
|
||
});
|
||
}
|
||
|
||
function buildNextActions(supplement, remove) {
|
||
const actions = [];
|
||
supplement.slice(0, 5).forEach((row) => actions.push(row.suggestion.replace(/。$/, "")));
|
||
if (remove.length) actions.push("清理厨房工具、绿植、泛杂货等低效或高售后链接");
|
||
if (!actions.length) actions.push("补充更多商品数据后再做动作判断");
|
||
return actions.slice(0, 6);
|
||
}
|
||
|
||
function buildOneLineAdvice(categoryStats, supplement) {
|
||
const main = getMainCategory(categoryStats);
|
||
const top = supplement[0];
|
||
if (top) return `下一周优先围绕 ${top.model || top.title} 做配件补全,不要继续扩杂货。`;
|
||
return main ? `下一周先围绕 ${main} 做标题和主图优化。` : "先补充近 7 天或近 30 天商品数据。";
|
||
}
|
||
|
||
function getTotals(rows) {
|
||
return rows.reduce((acc, row) => {
|
||
acc.payOrders += row.payOrders;
|
||
acc.payAmount += row.payAmount;
|
||
acc.refundOrders += row.successRefundOrders;
|
||
acc.refundAmount += row.successRefundAmount;
|
||
return acc;
|
||
}, { payOrders: 0, payAmount: 0, refundOrders: 0, refundAmount: 0 });
|
||
}
|
||
|
||
function getMainCategory(categoryStats) {
|
||
return categoryStats.length ? categoryStats[0].category : "";
|
||
}
|
||
|
||
function updateMetrics() {
|
||
const totals = getTotals(state.rows);
|
||
els.metricItems.textContent = state.rows.length;
|
||
els.metricOrders.textContent = totals.payOrders;
|
||
els.metricAmount.textContent = money(totals.payAmount);
|
||
els.metricRefund.textContent = money(totals.refundAmount);
|
||
els.metricCategory.textContent = getMainCategory(state.results.categoryStats || []) || "-";
|
||
}
|
||
|
||
function renderTab() {
|
||
if (!state.rows.length) {
|
||
renderEmpty();
|
||
return;
|
||
}
|
||
if (state.activeTab === "supplement") renderResultTable("补品清单", state.results.supplement, "符合成交稳定、退款低、型号明确的商品线。");
|
||
if (state.activeTab === "optimize") renderResultTable("优化清单", state.results.optimize, "有流量或询单但没有成交,先改标题、主图、价格和适配说明。");
|
||
if (state.activeTab === "remove") renderResultTable("下架清单", state.results.remove, "退款高、非主线或低效曝光商品,优先收缩货盘。");
|
||
if (state.activeTab === "review") renderReview();
|
||
if (state.activeTab === "data") renderData();
|
||
}
|
||
|
||
function renderEmpty() {
|
||
els.tabBody.innerHTML = '<div class="empty">粘贴数据后点击“分析数据”,这里会生成三张清单和周报。</div>';
|
||
}
|
||
|
||
function renderResultTable(title, rows, summary) {
|
||
const empty = rows.length ? "" : '<div class="empty">暂无符合条件的商品。</div>';
|
||
const table = rows.length ? `
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>优先级</th>
|
||
<th>商品</th>
|
||
<th>型号/配件</th>
|
||
<th>关键数据</th>
|
||
<th>判断</th>
|
||
<th>操作建议</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${rows.map(resultRowHtml).join("")}
|
||
</tbody>
|
||
</table>
|
||
</div>` : "";
|
||
els.tabBody.innerHTML = `
|
||
<div class="summary-line">${title}:${rows.length} 项。${summary}</div>
|
||
${empty}
|
||
${table}
|
||
`;
|
||
}
|
||
|
||
function resultRowHtml(row) {
|
||
return `
|
||
<tr>
|
||
<td><span class="badge ${row.priority.toLowerCase()}">${row.priority}</span></td>
|
||
<td class="title-cell">
|
||
<strong>${escapeHtml(row.title)}</strong>
|
||
<div class="muted">ID ${row.itemId} · ${row.category}</div>
|
||
</td>
|
||
<td>${escapeHtml(row.model || "-")}<div class="muted">${escapeHtml(row.accessoryType || "-")}</div></td>
|
||
<td>
|
||
${row.payOrders} 单 / ${money(row.payAmount)} 元
|
||
<div class="muted">曝光 ${row.exposurePeople} · 浏览 ${row.viewPeople} · 询单 ${row.inquiryPeople} · 退款 ${row.successRefundOrders}</div>
|
||
</td>
|
||
<td>${escapeHtml(row.reason)}</td>
|
||
<td>${escapeHtml(row.suggestion)}</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
|
||
function renderReview() {
|
||
const markdown = state.results.markdown || "";
|
||
els.tabBody.innerHTML = `
|
||
<div class="summary-line">周报 Markdown 已生成,可复制或下载到 Obsidian。</div>
|
||
<textarea class="markdown-box" readonly>${escapeHtml(markdown)}</textarea>
|
||
`;
|
||
}
|
||
|
||
function renderData() {
|
||
const categoryCards = (state.results.categoryStats || []).map((item) => `
|
||
<div class="note-card">
|
||
<h3>${escapeHtml(item.category)}</h3>
|
||
<p>${item.items} 个商品,${item.payOrders} 单,${money(item.payAmount)} 元,退款 ${money(item.refundAmount)} 元,浏览支付率 ${percent(item.payRate)}。</p>
|
||
</div>
|
||
`).join("");
|
||
|
||
const rows = state.rows.map((row) => `
|
||
<tr>
|
||
<td>${escapeHtml(row.itemId)}</td>
|
||
<td class="title-cell">${escapeHtml(row.title)}</td>
|
||
<td>${escapeHtml(row.category)}</td>
|
||
<td>${escapeHtml(row.model || "-")}</td>
|
||
<td>${escapeHtml(row.accessoryType || "-")}</td>
|
||
<td>${row.exposurePeople}</td>
|
||
<td>${row.viewPeople}</td>
|
||
<td>${row.inquiryPeople}</td>
|
||
<td>${row.payOrders}</td>
|
||
<td>${money(row.payAmount)}</td>
|
||
<td>${row.successRefundOrders}</td>
|
||
</tr>
|
||
`).join("");
|
||
|
||
els.tabBody.innerHTML = `
|
||
<div class="summary-line">解析数据:${state.rows.length} 个商品。字段解析有误时,可以调整粘贴格式后重新分析。</div>
|
||
<div class="cards-grid">${categoryCards}</div>
|
||
<div style="height:12px"></div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>商品ID</th>
|
||
<th>标题</th>
|
||
<th>类目</th>
|
||
<th>型号</th>
|
||
<th>配件</th>
|
||
<th>曝光</th>
|
||
<th>浏览</th>
|
||
<th>询单</th>
|
||
<th>订单</th>
|
||
<th>金额</th>
|
||
<th>退款单</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function showToast(message) {
|
||
els.toast.textContent = message;
|
||
els.toast.classList.add("show");
|
||
setTimeout(() => els.toast.classList.remove("show"), 1800);
|
||
}
|
||
|
||
function money(value) {
|
||
return Number(value || 0).toFixed(2).replace(/\.00$/, "");
|
||
}
|
||
|
||
function percent(value) {
|
||
return `${((value || 0) * 100).toFixed(2)}%`;
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value || "")
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
function escapeMd(value) {
|
||
return String(value || "").replace(/\|/g, "/").replace(/\n/g, " ");
|
||
}
|
||
|
||
els.collectorScript.value = getCollectorScript();
|
||
updateMetrics();
|
||
</script>
|
||
</body>
|
||
</html>
|