1408 lines
44 KiB
HTML
1408 lines
44 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>小鱼运营决策助手 v0.1</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f4f6f8;
|
||
--surface: #ffffff;
|
||
--surface-2: #f9fafb;
|
||
--text: #17202a;
|
||
--muted: #647184;
|
||
--line: #d8dee8;
|
||
--primary: #145c72;
|
||
--primary-2: #0f4657;
|
||
--accent: #b85c00;
|
||
--good: #087a55;
|
||
--warn: #9a5b00;
|
||
--bad: #b42318;
|
||
--info: #255e9c;
|
||
--soft-good: #e8f6ef;
|
||
--soft-warn: #fff3db;
|
||
--soft-bad: #fdecea;
|
||
--soft-info: #e9f2fb;
|
||
--shadow: 0 10px 28px rgba(23, 32, 42, 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: 20;
|
||
background: var(--surface);
|
||
border-bottom: 1px solid var(--line);
|
||
box-shadow: 0 2px 12px rgba(23, 32, 42, 0.04);
|
||
}
|
||
|
||
.topbar {
|
||
max-width: 1480px;
|
||
margin: 0 auto;
|
||
padding: 14px 22px;
|
||
display: grid;
|
||
grid-template-columns: minmax(260px, 1fr) auto;
|
||
gap: 18px;
|
||
align-items: center;
|
||
}
|
||
|
||
h1 {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
line-height: 1.2;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.subtitle {
|
||
margin-top: 4px;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.toolbar,
|
||
.button-row,
|
||
.tabs,
|
||
.filters {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
button,
|
||
.file-button {
|
||
min-height: 36px;
|
||
border: 1px solid var(--line);
|
||
background: var(--surface);
|
||
color: var(--text);
|
||
border-radius: 6px;
|
||
padding: 7px 12px;
|
||
font: inherit;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 7px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
button:hover,
|
||
.file-button:hover {
|
||
border-color: #aab6c4;
|
||
background: #f7f9fb;
|
||
}
|
||
|
||
button.primary {
|
||
background: var(--primary);
|
||
border-color: var(--primary);
|
||
color: #fff;
|
||
}
|
||
|
||
button.primary:hover {
|
||
background: var(--primary-2);
|
||
border-color: var(--primary-2);
|
||
}
|
||
|
||
button.ghost {
|
||
background: transparent;
|
||
}
|
||
|
||
button.danger {
|
||
color: var(--bad);
|
||
border-color: #efc6c1;
|
||
}
|
||
|
||
.icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
display: inline-block;
|
||
flex: 0 0 16px;
|
||
stroke: currentColor;
|
||
stroke-width: 2;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
fill: none;
|
||
}
|
||
|
||
main {
|
||
max-width: 1480px;
|
||
margin: 0 auto;
|
||
padding: 18px 22px 36px;
|
||
display: grid;
|
||
grid-template-columns: 380px minmax(0, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.panel {
|
||
background: var(--surface);
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.panel-head {
|
||
min-height: 52px;
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid var(--line);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.panel-title {
|
||
margin: 0;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.panel-body {
|
||
padding: 14px;
|
||
}
|
||
|
||
.left-stack,
|
||
.right-stack {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
min-width: 0;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
input,
|
||
textarea,
|
||
select {
|
||
width: 100%;
|
||
border: 1px solid var(--line);
|
||
border-radius: 6px;
|
||
background: #fff;
|
||
color: var(--text);
|
||
font: inherit;
|
||
padding: 8px 10px;
|
||
outline: none;
|
||
}
|
||
|
||
input:focus,
|
||
textarea:focus,
|
||
select:focus {
|
||
border-color: #6e9db0;
|
||
box-shadow: 0 0 0 3px rgba(20, 92, 114, 0.12);
|
||
}
|
||
|
||
textarea {
|
||
min-height: 260px;
|
||
resize: vertical;
|
||
font-family: Consolas, "Microsoft YaHei", monospace;
|
||
font-size: 12px;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
.grid-2 {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
}
|
||
|
||
.grid-3 {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.metric-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, minmax(130px, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.metric {
|
||
min-height: 82px;
|
||
padding: 12px;
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
background: var(--surface-2);
|
||
}
|
||
|
||
.metric strong {
|
||
display: block;
|
||
font-size: 22px;
|
||
line-height: 1.15;
|
||
letter-spacing: 0;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.metric span {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.tabs {
|
||
padding: 8px;
|
||
border-bottom: 1px solid var(--line);
|
||
background: var(--surface-2);
|
||
border-radius: 8px 8px 0 0;
|
||
}
|
||
|
||
.tab {
|
||
min-height: 34px;
|
||
background: transparent;
|
||
border-color: transparent;
|
||
padding: 6px 10px;
|
||
}
|
||
|
||
.tab.active {
|
||
background: var(--surface);
|
||
border-color: var(--line);
|
||
color: var(--primary);
|
||
box-shadow: 0 2px 8px rgba(23, 32, 42, 0.04);
|
||
}
|
||
|
||
.view {
|
||
display: none;
|
||
}
|
||
|
||
.view.active {
|
||
display: block;
|
||
}
|
||
|
||
.action-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.action-list {
|
||
min-height: 220px;
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
}
|
||
|
||
.action-head {
|
||
padding: 10px 12px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
background: var(--surface-2);
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
|
||
.action-head.good {
|
||
color: var(--good);
|
||
background: var(--soft-good);
|
||
}
|
||
|
||
.action-head.warn {
|
||
color: var(--warn);
|
||
background: var(--soft-warn);
|
||
}
|
||
|
||
.action-head.bad {
|
||
color: var(--bad);
|
||
background: var(--soft-bad);
|
||
}
|
||
|
||
.action-item {
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
|
||
.action-item:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.item-title {
|
||
font-weight: 700;
|
||
margin-bottom: 3px;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.item-meta {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.item-reason {
|
||
margin-top: 6px;
|
||
color: #384557;
|
||
font-size: 13px;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
min-height: 24px;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--line);
|
||
background: #fff;
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.pill.good {
|
||
color: var(--good);
|
||
background: var(--soft-good);
|
||
border-color: #bfe5d2;
|
||
}
|
||
|
||
.pill.warn {
|
||
color: var(--warn);
|
||
background: var(--soft-warn);
|
||
border-color: #f1d39a;
|
||
}
|
||
|
||
.pill.bad {
|
||
color: var(--bad);
|
||
background: var(--soft-bad);
|
||
border-color: #efc6c1;
|
||
}
|
||
|
||
.pill.info {
|
||
color: var(--info);
|
||
background: var(--soft-info);
|
||
border-color: #c5d9ee;
|
||
}
|
||
|
||
.table-wrap {
|
||
overflow: auto;
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
min-width: 980px;
|
||
border-collapse: collapse;
|
||
font-size: 13px;
|
||
}
|
||
|
||
th,
|
||
td {
|
||
padding: 9px 10px;
|
||
border-bottom: 1px solid var(--line);
|
||
text-align: left;
|
||
vertical-align: top;
|
||
}
|
||
|
||
th {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
background: #edf1f5;
|
||
color: #384557;
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
td.number,
|
||
th.number {
|
||
text-align: right;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
td.title-cell {
|
||
min-width: 260px;
|
||
max-width: 420px;
|
||
overflow-wrap: anywhere;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.mini-input {
|
||
width: 74px;
|
||
min-height: 30px;
|
||
padding: 5px 7px;
|
||
font-size: 12px;
|
||
text-align: right;
|
||
}
|
||
|
||
.empty {
|
||
padding: 22px;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
background: var(--surface-2);
|
||
}
|
||
|
||
.notice {
|
||
border: 1px solid #c5d9ee;
|
||
background: var(--soft-info);
|
||
color: #244866;
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.rule-list {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.rule-row {
|
||
display: grid;
|
||
grid-template-columns: minmax(130px, 180px) 1fr;
|
||
gap: 10px;
|
||
align-items: center;
|
||
border-bottom: 1px solid var(--line);
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.rule-row:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.rule-key {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.markdown-box {
|
||
min-height: 440px;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.status-line {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.hidden {
|
||
display: none !important;
|
||
}
|
||
|
||
@media (max-width: 1120px) {
|
||
main {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.metric-grid,
|
||
.action-grid {
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.topbar {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
main {
|
||
padding: 12px;
|
||
}
|
||
|
||
.metric-grid,
|
||
.action-grid,
|
||
.grid-2,
|
||
.grid-3 {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.toolbar {
|
||
justify-content: stretch;
|
||
}
|
||
|
||
button,
|
||
.file-button {
|
||
flex: 1 1 auto;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="topbar">
|
||
<div>
|
||
<h1>小鱼运营决策助手 v0.1</h1>
|
||
<div class="subtitle">鱼小铺小天才配件 · 粘贴数据 · 利润测算 · 今日动作</div>
|
||
</div>
|
||
<div class="toolbar">
|
||
<button class="primary" id="analyzeBtn" type="button" title="解析粘贴的数据并生成建议">
|
||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/></svg>
|
||
分析
|
||
</button>
|
||
<button id="sampleBtn" type="button" title="填入一组样例商品数据">
|
||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8"/><path d="M8 17h5"/></svg>
|
||
样例
|
||
</button>
|
||
<button id="copyBtn" type="button" title="复制复盘 Markdown">
|
||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||
复制
|
||
</button>
|
||
<button id="downloadBtn" type="button" title="下载复盘 Markdown">
|
||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
|
||
下载
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<section class="left-stack">
|
||
<div class="panel">
|
||
<div class="panel-head">
|
||
<h2 class="panel-title">数据粘贴区</h2>
|
||
<span class="status-line" id="parseStatus">未分析</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<textarea id="rawInput" spellcheck="false" placeholder="商品标题 曝光人数 浏览人数 询单人数 支付订单 支付金额
|
||
D2A/Q2A 表带 蓝色 285 16 6 4 32.40"></textarea>
|
||
<div class="button-row" style="margin-top:10px">
|
||
<label class="file-button" for="fileInput" title="导入 CSV 或 TXT">
|
||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v12"/></svg>
|
||
导入
|
||
</label>
|
||
<input class="hidden" id="fileInput" type="file" accept=".csv,.txt,.tsv">
|
||
<button id="pasteBtn" type="button" title="读取剪贴板文本">
|
||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/></svg>
|
||
粘贴
|
||
</button>
|
||
<button class="danger" id="clearBtn" type="button" title="清空当前输入和分析结果">
|
||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
|
||
清空
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-head">
|
||
<h2 class="panel-title">测算参数</h2>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="grid-2">
|
||
<div>
|
||
<label for="feeRate">扣费比例</label>
|
||
<input id="feeRate" type="number" min="0" step="0.1" value="4">
|
||
</div>
|
||
<div>
|
||
<label for="packCost">包装成本/单</label>
|
||
<input id="packCost" type="number" min="0" step="0.1" value="0.2">
|
||
</div>
|
||
</div>
|
||
<div class="grid-2" style="margin-top:10px">
|
||
<div>
|
||
<label for="minProfit">扩品净利底线</label>
|
||
<input id="minProfit" type="number" min="0" step="0.1" value="3">
|
||
</div>
|
||
<div>
|
||
<label for="thinProfit">低利润预警</label>
|
||
<input id="thinProfit" type="number" min="0" step="0.1" value="2">
|
||
</div>
|
||
</div>
|
||
<div class="notice" style="margin-top:12px">
|
||
默认成本:普通表带 4.00,带螺丝杆 6.80。未识别成本时按 4.00 先估,表格里可单独改。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-head">
|
||
<h2 class="panel-title">成本库</h2>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="grid-2">
|
||
<div>
|
||
<label for="strapCost">普通表带</label>
|
||
<input id="strapCost" type="number" min="0" step="0.1" value="4">
|
||
</div>
|
||
<div>
|
||
<label for="screwCost">带螺丝杆</label>
|
||
<input id="screwCost" type="number" min="0" step="0.1" value="6.8">
|
||
</div>
|
||
<div>
|
||
<label for="wireCost">充电线</label>
|
||
<input id="wireCost" type="number" min="0" step="0.1" value="4">
|
||
</div>
|
||
<div>
|
||
<label for="caseCost">保护壳</label>
|
||
<input id="caseCost" type="number" min="0" step="0.1" value="4">
|
||
</div>
|
||
<div>
|
||
<label for="lanyardCost">挂绳/保护套</label>
|
||
<input id="lanyardCost" type="number" min="0" step="0.1" value="4">
|
||
</div>
|
||
<div>
|
||
<label for="stoolCost">小马扎</label>
|
||
<input id="stoolCost" type="number" min="0" step="0.1" value="9.2">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="right-stack">
|
||
<div class="metric-grid">
|
||
<div class="metric">
|
||
<strong id="mItems">0</strong>
|
||
<span>商品数</span>
|
||
</div>
|
||
<div class="metric">
|
||
<strong id="mPaid">0</strong>
|
||
<span>支付订单</span>
|
||
</div>
|
||
<div class="metric">
|
||
<strong id="mAmount">0.00</strong>
|
||
<span>支付金额</span>
|
||
</div>
|
||
<div class="metric">
|
||
<strong id="mProfit">0.00</strong>
|
||
<span>估算净利</span>
|
||
</div>
|
||
<div class="metric">
|
||
<strong id="mFocus">0</strong>
|
||
<span>今日动作</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="tabs">
|
||
<button class="tab active" type="button" data-tab="actions">今日清单</button>
|
||
<button class="tab" type="button" data-tab="table">商品判断</button>
|
||
<button class="tab" type="button" data-tab="observe">7天观察</button>
|
||
<button class="tab" type="button" data-tab="rules">规则</button>
|
||
<button class="tab" type="button" data-tab="markdown">复盘</button>
|
||
</div>
|
||
|
||
<div class="panel-body">
|
||
<div class="view active" id="view-actions">
|
||
<div class="action-grid">
|
||
<div class="action-list">
|
||
<div class="action-head good">
|
||
<span>值得做</span>
|
||
<span class="pill good" id="countKeep">0</span>
|
||
</div>
|
||
<div id="keepList"></div>
|
||
</div>
|
||
<div class="action-list">
|
||
<div class="action-head warn">
|
||
<span>今晚优化</span>
|
||
<span class="pill warn" id="countOpt">0</span>
|
||
</div>
|
||
<div id="optList"></div>
|
||
</div>
|
||
<div class="action-list">
|
||
<div class="action-head bad">
|
||
<span>停扩/不值得</span>
|
||
<span class="pill bad" id="countStop">0</span>
|
||
</div>
|
||
<div id="stopList"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-table">
|
||
<div class="filters" style="margin-bottom:10px">
|
||
<select id="decisionFilter" style="max-width:180px">
|
||
<option value="all">全部判断</option>
|
||
<option value="值得做">值得做</option>
|
||
<option value="优化">优化</option>
|
||
<option value="观察">观察</option>
|
||
<option value="停扩">停扩</option>
|
||
</select>
|
||
<button id="recalcBtn" type="button" title="按当前成本重新测算">
|
||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg>
|
||
重算
|
||
</button>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>商品</th>
|
||
<th>型号</th>
|
||
<th>类型</th>
|
||
<th class="number">曝光</th>
|
||
<th class="number">浏览</th>
|
||
<th class="number">询单</th>
|
||
<th class="number">支付</th>
|
||
<th class="number">金额</th>
|
||
<th class="number">成本/单</th>
|
||
<th class="number">净利/单</th>
|
||
<th>判断</th>
|
||
<th>建议动作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="resultBody">
|
||
<tr><td colspan="12" class="empty">粘贴商品数据后点击“分析”。</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-observe">
|
||
<div class="notice" style="margin-bottom:12px">
|
||
自动挑出适合进入 7 天观察的商品:新补品、改价款、高曝光无成交、成交但净利偏薄。
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>观察对象</th>
|
||
<th>观察原因</th>
|
||
<th>今天只改一个动作</th>
|
||
<th class="number">7天目标</th>
|
||
<th>结论口径</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="observeBody">
|
||
<tr><td colspan="5" class="empty">暂无观察对象。</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-rules">
|
||
<div class="rule-list">
|
||
<div class="rule-row">
|
||
<div class="rule-key">主线</div>
|
||
<div>D3、D2A/Q2A、D2/Q1A 小天才配件优先;副线只保留小马扎、挂绳、Z6Pro、Z7/Z7S 已验证商品。</div>
|
||
</div>
|
||
<div class="rule-row">
|
||
<div class="rule-key">扩品</div>
|
||
<div>有成交且净利不低于扩品底线,优先补同型号充电线、保护壳、带工具款和组合装。</div>
|
||
</div>
|
||
<div class="rule-row">
|
||
<div class="rule-key">优化</div>
|
||
<div>曝光高浏览低先换主图;浏览高询单低补详情;询单高成交低看价格、话术和发货承诺。</div>
|
||
</div>
|
||
<div class="rule-row">
|
||
<div class="rule-key">停扩</div>
|
||
<div>净利低于低利润预警、高曝光无成交、非主线散货、买错型号/质量售后风险,先停止扩链接。</div>
|
||
</div>
|
||
<div class="rule-row">
|
||
<div class="rule-key">今晚节奏</div>
|
||
<div>最多 3 个动作:先处理待发货/售后,再优化 1~3 个链接,最后只补 1~3 个高相关品。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-markdown">
|
||
<textarea id="markdownOutput" class="markdown-box" spellcheck="false" readonly></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<script>
|
||
const $ = (id) => document.getElementById(id);
|
||
const money = (n) => (Number.isFinite(n) ? n : 0).toFixed(2);
|
||
const pct = (n) => `${((Number.isFinite(n) ? n : 0) * 100).toFixed(1)}%`;
|
||
|
||
const sampleText = `商品标题\t曝光人数\t浏览人数\t询单人数\t支付订单\t支付金额
|
||
D2A/Q2A 表带 蓝色\t285\t16\t6\t4\t32.40
|
||
D3 表带 晴山蓝\t272\t7\t3\t0\t0
|
||
D3 第二表带链接\t58\t1\t1\t1\t8.50
|
||
红色折叠便携小马扎\t218\t9\t1\t1\t10.50
|
||
斜挎包折叠凳二合一\t334\t7\t0\t0\t0
|
||
D2A/Q2A 磁吸充电线\t98\t3\t1\t1\t6.80
|
||
儿童钓鱼竿 套装\t2258\t58\t0\t0\t0
|
||
小米手环10 表带\t1278\t143\t0\t0\t0`;
|
||
|
||
let rows = [];
|
||
let analyzedRows = [];
|
||
|
||
const stateFields = [
|
||
"rawInput", "feeRate", "packCost", "minProfit", "thinProfit",
|
||
"strapCost", "screwCost", "wireCost", "caseCost", "lanyardCost", "stoolCost"
|
||
];
|
||
|
||
function readNumber(value) {
|
||
if (value === null || value === undefined) return 0;
|
||
const normalized = String(value)
|
||
.replace(/,/g, "")
|
||
.replace(/¥/g, "")
|
||
.replace(/元/g, "")
|
||
.replace(/%/g, "")
|
||
.trim();
|
||
const match = normalized.match(/-?\d+(?:\.\d+)?/);
|
||
return match ? Number(match[0]) : 0;
|
||
}
|
||
|
||
function settings() {
|
||
return {
|
||
feeRate: readNumber($("feeRate").value) / 100,
|
||
packCost: readNumber($("packCost").value),
|
||
minProfit: readNumber($("minProfit").value),
|
||
thinProfit: readNumber($("thinProfit").value),
|
||
strapCost: readNumber($("strapCost").value),
|
||
screwCost: readNumber($("screwCost").value),
|
||
wireCost: readNumber($("wireCost").value),
|
||
caseCost: readNumber($("caseCost").value),
|
||
lanyardCost: readNumber($("lanyardCost").value),
|
||
stoolCost: readNumber($("stoolCost").value)
|
||
};
|
||
}
|
||
|
||
function saveState() {
|
||
const data = {};
|
||
stateFields.forEach((id) => data[id] = $(id).value);
|
||
localStorage.setItem("xiaoyu-decision-v01", JSON.stringify(data));
|
||
}
|
||
|
||
function loadState() {
|
||
try {
|
||
const data = JSON.parse(localStorage.getItem("xiaoyu-decision-v01") || "{}");
|
||
stateFields.forEach((id) => {
|
||
if (data[id] !== undefined) $(id).value = data[id];
|
||
});
|
||
} catch (error) {
|
||
console.warn(error);
|
||
}
|
||
}
|
||
|
||
function splitLine(line) {
|
||
const trimmed = line.trim();
|
||
if (!trimmed) return [];
|
||
if (trimmed.includes("\t")) return trimmed.split("\t").map((v) => v.trim());
|
||
if (trimmed.includes(",")) return parseCsvLine(trimmed);
|
||
return trimmed.split(/\s{2,}/).map((v) => v.trim());
|
||
}
|
||
|
||
function parseCsvLine(line) {
|
||
const cells = [];
|
||
let cell = "";
|
||
let quoted = false;
|
||
for (let i = 0; i < line.length; i += 1) {
|
||
const ch = line[i];
|
||
if (ch === '"' && line[i + 1] === '"') {
|
||
cell += '"';
|
||
i += 1;
|
||
} else if (ch === '"') {
|
||
quoted = !quoted;
|
||
} else if (ch === "," && !quoted) {
|
||
cells.push(cell.trim());
|
||
cell = "";
|
||
} else {
|
||
cell += ch;
|
||
}
|
||
}
|
||
cells.push(cell.trim());
|
||
return cells;
|
||
}
|
||
|
||
function normalizeHeader(text) {
|
||
return String(text || "")
|
||
.toLowerCase()
|
||
.replace(/\s/g, "")
|
||
.replace(/[()()]/g, "");
|
||
}
|
||
|
||
function columnIndex(headers, keys) {
|
||
const normalized = headers.map(normalizeHeader);
|
||
for (const key of keys) {
|
||
const index = normalized.findIndex((header) => header.includes(key));
|
||
if (index >= 0) return index;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
function parseInput(text) {
|
||
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
||
if (!lines.length) return [];
|
||
|
||
const first = splitLine(lines[0]);
|
||
const hasHeader = first.some((cell) => /商品|标题|曝光|浏览|询单|支付|金额|转化/.test(cell));
|
||
const headers = hasHeader ? first : [];
|
||
const dataLines = hasHeader ? lines.slice(1) : lines;
|
||
|
||
const idx = {
|
||
title: columnIndex(headers, ["商品标题", "标题", "商品", "宝贝"]),
|
||
exposure: columnIndex(headers, ["曝光人数", "曝光"]),
|
||
view: columnIndex(headers, ["浏览人数", "浏览"]),
|
||
inquiry: columnIndex(headers, ["询单人数", "询单", "咨询"]),
|
||
paid: columnIndex(headers, ["支付订单", "支付件数", "支付人数", "支付"]),
|
||
amount: columnIndex(headers, ["支付金额", "成交金额", "金额"]),
|
||
refund: columnIndex(headers, ["退款", "售后"])
|
||
};
|
||
|
||
return dataLines.map((line, index) => {
|
||
const cells = splitLine(line);
|
||
if (!cells.length) return null;
|
||
if (!hasHeader && cells.length > 1 && cells.slice(1).every((cell) => readNumber(cell) === 0) && /商品|标题|曝光|浏览|询单|支付|金额/.test(cells.join(""))) {
|
||
return null;
|
||
}
|
||
|
||
if (headers.length) {
|
||
return {
|
||
id: index,
|
||
raw: line,
|
||
title: cells[idx.title >= 0 ? idx.title : 0] || `商品 ${index + 1}`,
|
||
exposure: readNumber(cells[idx.exposure]),
|
||
view: readNumber(cells[idx.view]),
|
||
inquiry: readNumber(cells[idx.inquiry]),
|
||
paid: readNumber(cells[idx.paid]),
|
||
amount: readNumber(cells[idx.amount]),
|
||
refund: readNumber(cells[idx.refund])
|
||
};
|
||
}
|
||
|
||
const numbers = cells.map(readNumber);
|
||
return {
|
||
id: index,
|
||
raw: line,
|
||
title: cells[0] || `商品 ${index + 1}`,
|
||
exposure: numbers[1] || 0,
|
||
view: numbers[2] || 0,
|
||
inquiry: numbers[3] || 0,
|
||
paid: numbers[4] || 0,
|
||
amount: numbers[5] || 0,
|
||
refund: numbers[6] || 0
|
||
};
|
||
}).filter(Boolean);
|
||
}
|
||
|
||
function inferModel(title) {
|
||
const t = title.toUpperCase();
|
||
if (/D2A|Q2A/.test(t)) return "D2A/Q2A";
|
||
if (/D2|Q1A/.test(t)) return "D2/Q1A";
|
||
if (/D3/.test(t)) return "D3";
|
||
if (/Z6\s*PRO|Z6PRO/.test(t)) return "Z6Pro";
|
||
if (/Z7|Z7S/.test(t)) return "Z7/Z7S";
|
||
if (/小天才/.test(title)) return "小天才";
|
||
if (/小米|手环10|9PRO|8PRO/i.test(title)) return "小米手环";
|
||
if (/苹果|APPLE|S10|S11/i.test(title)) return "苹果表带";
|
||
return "其他";
|
||
}
|
||
|
||
function inferType(title) {
|
||
if (/充电|磁吸|数据线/.test(title)) return "充电线";
|
||
if (/保护壳|保护套|壳/.test(title)) return "保护壳";
|
||
if (/螺丝|螺杆|拆装|工具/.test(title)) return "带工具";
|
||
if (/表带|腕带/.test(title)) return "表带";
|
||
if (/挂绳|防丢|挂脖/.test(title)) return "挂绳";
|
||
if (/马扎|折叠凳|凳/.test(title)) return "小马扎";
|
||
if (/钓鱼|鱼竿/.test(title)) return "钓鱼竿";
|
||
if (/厨房|拔毛|削皮|工具/.test(title)) return "厨房工具";
|
||
if (/绿植|盆栽|花盆/.test(title)) return "绿植";
|
||
return "其他";
|
||
}
|
||
|
||
function inferCost(row, s) {
|
||
const title = row.title;
|
||
const type = row.type;
|
||
if (/螺丝|螺杆|拆装|工具/.test(title) && /表带|腕带|D2|D3|Q2A|Q1A/i.test(title)) return s.screwCost;
|
||
if (type === "表带") return s.strapCost;
|
||
if (type === "充电线") return s.wireCost;
|
||
if (type === "保护壳") return s.caseCost;
|
||
if (type === "挂绳") return s.lanyardCost;
|
||
if (type === "小马扎") return s.stoolCost;
|
||
return s.strapCost;
|
||
}
|
||
|
||
function isMainLine(row) {
|
||
return ["D3", "D2A/Q2A", "D2/Q1A"].includes(row.model);
|
||
}
|
||
|
||
function isSideLine(row) {
|
||
return ["Z6Pro", "Z7/Z7S"].includes(row.model) || ["小马扎", "挂绳"].includes(row.type);
|
||
}
|
||
|
||
function isNoGo(row) {
|
||
return ["钓鱼竿", "厨房工具", "绿植"].includes(row.type) || ["小米手环", "苹果表带"].includes(row.model);
|
||
}
|
||
|
||
function analyzeRow(inputRow, index) {
|
||
const s = settings();
|
||
const row = { ...inputRow, id: index };
|
||
row.model = inferModel(row.title);
|
||
row.type = inferType(row.title);
|
||
row.cost = row.costOverride ?? inferCost(row, s);
|
||
row.unitPrice = row.paid > 0 ? row.amount / row.paid : 0;
|
||
row.fee = row.amount * s.feeRate;
|
||
row.totalCost = row.paid * (row.cost + s.packCost);
|
||
row.profit = row.amount - row.fee - row.totalCost;
|
||
row.profitPerOrder = row.paid > 0 ? row.profit / row.paid : 0;
|
||
row.viewRate = row.exposure > 0 ? row.view / row.exposure : 0;
|
||
row.inquiryRate = row.view > 0 ? row.inquiry / row.view : 0;
|
||
row.payRate = row.view > 0 ? row.paid / row.view : 0;
|
||
row.mainLine = isMainLine(row);
|
||
row.sideLine = isSideLine(row);
|
||
row.noGo = isNoGo(row);
|
||
row.reasons = [];
|
||
row.action = "";
|
||
row.decision = "观察";
|
||
|
||
if (row.paid > 0 && row.profitPerOrder >= s.minProfit && (row.mainLine || row.sideLine)) {
|
||
row.decision = "值得做";
|
||
row.action = row.mainLine ? "保留并扩同型号高利润规格" : "副线保留,不扩大矩阵";
|
||
row.reasons.push(`有成交,净利/单 ${money(row.profitPerOrder)} 元`);
|
||
} else if (row.paid > 0 && row.profitPerOrder >= s.thinProfit) {
|
||
row.decision = "优化";
|
||
row.action = "可卖但偏薄,优先做组合装或提价";
|
||
row.reasons.push(`有成交但净利/单 ${money(row.profitPerOrder)} 元`);
|
||
} else if (row.paid > 0 && row.profitPerOrder < s.thinProfit) {
|
||
row.decision = "停扩";
|
||
row.action = "停止扩同款,先涨价或换高利润规格";
|
||
row.reasons.push(`净利/单低于 ${money(s.thinProfit)} 元`);
|
||
}
|
||
|
||
if (row.noGo && row.paid === 0) {
|
||
row.decision = "停扩";
|
||
row.action = "非当前主线,停止扩链接";
|
||
row.reasons.push("属于当前不扩方向");
|
||
}
|
||
|
||
if (row.paid === 0 && row.exposure >= 500 && row.viewRate < 0.03) {
|
||
row.decision = row.noGo ? "停扩" : "优化";
|
||
row.action = row.noGo ? "不扩,必要时下架或降权" : "先换主图或重写标题前 20 字";
|
||
row.reasons.push(`曝光高但浏览率只有 ${pct(row.viewRate)}`);
|
||
} else if (row.paid === 0 && row.view >= 10 && row.inquiryRate < 0.1) {
|
||
row.decision = row.noGo ? "停扩" : "优化";
|
||
row.action = row.noGo ? "不扩,减少维护" : "补详情中的适配型号、包含物、发货说明";
|
||
row.reasons.push(`浏览有量但询单率 ${pct(row.inquiryRate)}`);
|
||
} else if (row.paid === 0 && row.inquiry > 0) {
|
||
row.decision = row.noGo ? "停扩" : "优化";
|
||
row.action = row.noGo ? "不扩,避免占精力" : "查价格、发货承诺和咨询话术";
|
||
row.reasons.push("有询单无成交");
|
||
}
|
||
|
||
if (row.paid === 0 && row.mainLine && row.exposure < 50) {
|
||
row.decision = "观察";
|
||
row.action = "先改标题关键词,再进 7 天观察";
|
||
row.reasons.push("主线但曝光不足");
|
||
}
|
||
|
||
if (!row.reasons.length) {
|
||
row.reasons.push("数据不足,先观察");
|
||
row.action = row.mainLine ? "保留观察,暂不乱改" : "不占用今晚主动作";
|
||
}
|
||
|
||
row.observe = shouldObserve(row, s);
|
||
return row;
|
||
}
|
||
|
||
function shouldObserve(row, s) {
|
||
if (row.decision === "优化") return true;
|
||
if (row.decision === "停扩" && row.exposure >= 500 && !row.noGo) return true;
|
||
if (row.mainLine && row.paid > 0 && row.profitPerOrder < s.minProfit) return true;
|
||
if (row.mainLine && ["充电线", "保护壳", "带工具"].includes(row.type)) return true;
|
||
return false;
|
||
}
|
||
|
||
function analyze() {
|
||
rows = parseInput($("rawInput").value);
|
||
analyzedRows = rows.map(analyzeRow);
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function render() {
|
||
renderMetrics();
|
||
renderActions();
|
||
renderTable();
|
||
renderObserve();
|
||
renderMarkdown();
|
||
$("parseStatus").textContent = analyzedRows.length ? `已分析 ${analyzedRows.length} 条` : "未分析";
|
||
}
|
||
|
||
function renderMetrics() {
|
||
const totalPaid = analyzedRows.reduce((sum, row) => sum + row.paid, 0);
|
||
const totalAmount = analyzedRows.reduce((sum, row) => sum + row.amount, 0);
|
||
const totalProfit = analyzedRows.reduce((sum, row) => sum + row.profit, 0);
|
||
const focusCount = analyzedRows.filter((row) => ["值得做", "优化", "停扩"].includes(row.decision)).length;
|
||
$("mItems").textContent = analyzedRows.length;
|
||
$("mPaid").textContent = totalPaid;
|
||
$("mAmount").textContent = money(totalAmount);
|
||
$("mProfit").textContent = money(totalProfit);
|
||
$("mFocus").textContent = focusCount;
|
||
}
|
||
|
||
function topRows(decision, limit) {
|
||
return analyzedRows
|
||
.filter((row) => row.decision === decision)
|
||
.sort((a, b) => {
|
||
if (decision === "值得做") return b.profit - a.profit || b.paid - a.paid;
|
||
if (decision === "优化") return b.exposure - a.exposure || b.inquiry - a.inquiry;
|
||
return b.exposure - a.exposure || a.profitPerOrder - b.profitPerOrder;
|
||
})
|
||
.slice(0, limit);
|
||
}
|
||
|
||
function renderActions() {
|
||
const keep = topRows("值得做", 5);
|
||
const opt = topRows("优化", 5);
|
||
const stop = topRows("停扩", 5);
|
||
$("countKeep").textContent = keep.length;
|
||
$("countOpt").textContent = opt.length;
|
||
$("countStop").textContent = stop.length;
|
||
renderActionList("keepList", keep, "good");
|
||
renderActionList("optList", opt, "warn");
|
||
renderActionList("stopList", stop, "bad");
|
||
}
|
||
|
||
function renderActionList(id, list, tone) {
|
||
if (!list.length) {
|
||
$(id).innerHTML = `<div class="empty">暂无</div>`;
|
||
return;
|
||
}
|
||
$(id).innerHTML = list.map((row) => `
|
||
<div class="action-item">
|
||
<div class="item-title">${escapeHtml(row.title)}</div>
|
||
<div class="item-meta">
|
||
<span class="pill ${tone}">${row.model}</span>
|
||
<span class="pill">${row.type}</span>
|
||
<span>支付 ${row.paid}</span>
|
||
<span>净利/单 ${money(row.profitPerOrder)}</span>
|
||
</div>
|
||
<div class="item-reason">${escapeHtml(row.action)}。${escapeHtml(row.reasons.join(";"))}</div>
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
function renderTable() {
|
||
const filter = $("decisionFilter").value;
|
||
const list = analyzedRows.filter((row) => filter === "all" || row.decision === filter);
|
||
if (!list.length) {
|
||
$("resultBody").innerHTML = `<tr><td colspan="12" class="empty">暂无匹配商品。</td></tr>`;
|
||
return;
|
||
}
|
||
$("resultBody").innerHTML = list.map((row) => `
|
||
<tr>
|
||
<td class="title-cell">${escapeHtml(row.title)}</td>
|
||
<td>${row.model}</td>
|
||
<td>${row.type}</td>
|
||
<td class="number">${row.exposure}</td>
|
||
<td class="number">${row.view}<br><span class="status-line">${pct(row.viewRate)}</span></td>
|
||
<td class="number">${row.inquiry}<br><span class="status-line">${pct(row.inquiryRate)}</span></td>
|
||
<td class="number">${row.paid}<br><span class="status-line">${pct(row.payRate)}</span></td>
|
||
<td class="number">${money(row.amount)}</td>
|
||
<td class="number">
|
||
<input class="mini-input" type="number" min="0" step="0.1" value="${money(row.cost)}" data-cost-id="${row.id}" title="修改该商品成本">
|
||
</td>
|
||
<td class="number">${money(row.profitPerOrder)}<br><span class="status-line">总 ${money(row.profit)}</span></td>
|
||
<td>${decisionPill(row.decision)}</td>
|
||
<td>${escapeHtml(row.action)}</td>
|
||
</tr>
|
||
`).join("");
|
||
|
||
document.querySelectorAll("[data-cost-id]").forEach((input) => {
|
||
input.addEventListener("change", (event) => {
|
||
const id = Number(event.target.dataset.costId);
|
||
const source = rows[id];
|
||
if (source) {
|
||
source.costOverride = readNumber(event.target.value);
|
||
analyzedRows = rows.map(analyzeRow);
|
||
render();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function decisionPill(decision) {
|
||
if (decision === "值得做") return `<span class="pill good">值得做</span>`;
|
||
if (decision === "优化") return `<span class="pill warn">优化</span>`;
|
||
if (decision === "停扩") return `<span class="pill bad">停扩</span>`;
|
||
return `<span class="pill info">观察</span>`;
|
||
}
|
||
|
||
function renderObserve() {
|
||
const list = analyzedRows.filter((row) => row.observe).slice(0, 12);
|
||
if (!list.length) {
|
||
$("observeBody").innerHTML = `<tr><td colspan="5" class="empty">暂无观察对象。</td></tr>`;
|
||
return;
|
||
}
|
||
$("observeBody").innerHTML = list.map((row) => `
|
||
<tr>
|
||
<td class="title-cell">${escapeHtml(row.title)}<br><span class="status-line">${row.model} / ${row.type}</span></td>
|
||
<td>${escapeHtml(row.reasons.join(";"))}</td>
|
||
<td>${escapeHtml(singleAction(row))}</td>
|
||
<td class="number">${observeTarget(row)}</td>
|
||
<td>${observeConclusion(row)}</td>
|
||
</tr>
|
||
`).join("");
|
||
}
|
||
|
||
function singleAction(row) {
|
||
if (row.paid > 0 && row.profitPerOrder < settings().minProfit) return "只改规格价或组合装";
|
||
if (row.exposure >= 500 && row.viewRate < 0.03) return "只换主图";
|
||
if (row.view >= 10 && row.inquiryRate < 0.1) return "只补适配说明";
|
||
if (row.inquiry > 0 && row.paid === 0) return "只调价格或话术";
|
||
if (row.exposure < 50) return "只改标题前 20 字";
|
||
return "只保留观察";
|
||
}
|
||
|
||
function observeTarget(row) {
|
||
if (row.paid > 0) return "净利>=3";
|
||
if (row.exposure < 50) return "曝光>=50";
|
||
if (row.viewRate < 0.03) return "浏览率>=3%";
|
||
if (row.inquiryRate < 0.1) return "询单率>=10%";
|
||
return "有询单/成交";
|
||
}
|
||
|
||
function observeConclusion(row) {
|
||
if (row.paid > 0 && row.profitPerOrder >= settings().minProfit) return "保留并扩同型号";
|
||
if (row.paid > 0) return "提价或做组合";
|
||
if (row.noGo) return "不扩";
|
||
return "7 天后按曝光、浏览、询单、成交复判";
|
||
}
|
||
|
||
function renderMarkdown() {
|
||
const today = new Date().toLocaleDateString("zh-CN", {
|
||
year: "numeric", month: "2-digit", day: "2-digit"
|
||
}).replace(/\//g, "-");
|
||
const keep = topRows("值得做", 5);
|
||
const opt = topRows("优化", 5);
|
||
const stop = topRows("停扩", 5);
|
||
const observe = analyzedRows.filter((row) => row.observe).slice(0, 8);
|
||
const totalPaid = analyzedRows.reduce((sum, row) => sum + row.paid, 0);
|
||
const totalAmount = analyzedRows.reduce((sum, row) => sum + row.amount, 0);
|
||
const totalProfit = analyzedRows.reduce((sum, row) => sum + row.profit, 0);
|
||
|
||
const md = `# 小鱼每日复盘_${today}
|
||
|
||
## 数据概览
|
||
|
||
- 商品数:${analyzedRows.length}
|
||
- 支付订单:${totalPaid}
|
||
- 支付金额:${money(totalAmount)}
|
||
- 估算净利:${money(totalProfit)}
|
||
|
||
## 值得做
|
||
|
||
${markdownRows(keep)}
|
||
|
||
## 今晚优化
|
||
|
||
${markdownRows(opt)}
|
||
|
||
## 停扩/不值得
|
||
|
||
${markdownRows(stop)}
|
||
|
||
## 进入 7 天观察
|
||
|
||
${observe.length ? observe.map((row) => `- ${row.title}:${singleAction(row)},目标 ${observeTarget(row)}`).join("\n") : "- 暂无"}
|
||
|
||
## 明天只看这 3 件事
|
||
|
||
- D3 / D2A-Q2A 是否继续成交
|
||
- 低利润 SKU 是否已经提价或改组合
|
||
- 高曝光无成交商品是否只改了一个变量
|
||
`;
|
||
$("markdownOutput").value = md;
|
||
}
|
||
|
||
function markdownRows(list) {
|
||
if (!list.length) return "- 暂无";
|
||
return list.map((row) => `- ${row.title}:${row.action};支付 ${row.paid},金额 ${money(row.amount)},净利/单 ${money(row.profitPerOrder)}`).join("\n");
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value ?? "")
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
async function copyMarkdown() {
|
||
if (!$("markdownOutput").value) renderMarkdown();
|
||
try {
|
||
await navigator.clipboard.writeText($("markdownOutput").value);
|
||
$("parseStatus").textContent = "复盘已复制";
|
||
} catch (error) {
|
||
$("markdownOutput").select();
|
||
document.execCommand("copy");
|
||
$("parseStatus").textContent = "复盘已复制";
|
||
}
|
||
}
|
||
|
||
function downloadMarkdown() {
|
||
if (!$("markdownOutput").value) renderMarkdown();
|
||
const blob = new Blob([$("markdownOutput").value], { type: "text/markdown;charset=utf-8" });
|
||
const url = URL.createObjectURL(blob);
|
||
const link = document.createElement("a");
|
||
const day = new Date().toISOString().slice(0, 10);
|
||
link.href = url;
|
||
link.download = `小鱼每日复盘_${day}.md`;
|
||
link.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
async function pasteFromClipboard() {
|
||
try {
|
||
const text = await navigator.clipboard.readText();
|
||
$("rawInput").value = text;
|
||
analyze();
|
||
} catch (error) {
|
||
$("parseStatus").textContent = "浏览器未允许读取剪贴板";
|
||
}
|
||
}
|
||
|
||
function bindEvents() {
|
||
$("analyzeBtn").addEventListener("click", analyze);
|
||
$("sampleBtn").addEventListener("click", () => {
|
||
$("rawInput").value = sampleText;
|
||
analyze();
|
||
});
|
||
$("copyBtn").addEventListener("click", copyMarkdown);
|
||
$("downloadBtn").addEventListener("click", downloadMarkdown);
|
||
$("pasteBtn").addEventListener("click", pasteFromClipboard);
|
||
$("clearBtn").addEventListener("click", () => {
|
||
$("rawInput").value = "";
|
||
rows = [];
|
||
analyzedRows = [];
|
||
saveState();
|
||
render();
|
||
});
|
||
$("recalcBtn").addEventListener("click", analyze);
|
||
$("decisionFilter").addEventListener("change", renderTable);
|
||
$("fileInput").addEventListener("change", async (event) => {
|
||
const file = event.target.files && event.target.files[0];
|
||
if (!file) return;
|
||
$("rawInput").value = await file.text();
|
||
analyze();
|
||
event.target.value = "";
|
||
});
|
||
|
||
stateFields.forEach((id) => {
|
||
$(id).addEventListener("input", saveState);
|
||
});
|
||
|
||
document.querySelectorAll(".tab").forEach((tab) => {
|
||
tab.addEventListener("click", () => {
|
||
document.querySelectorAll(".tab").forEach((item) => item.classList.remove("active"));
|
||
document.querySelectorAll(".view").forEach((view) => view.classList.remove("active"));
|
||
tab.classList.add("active");
|
||
$(`view-${tab.dataset.tab}`).classList.add("active");
|
||
if (tab.dataset.tab === "markdown") renderMarkdown();
|
||
});
|
||
});
|
||
}
|
||
|
||
loadState();
|
||
bindEvents();
|
||
if ($("rawInput").value.trim()) analyze();
|
||
</script>
|
||
</body>
|
||
</html>
|