Files
ObsidianHome/小鱼/小鱼运营决策助手_v0.1.html
T

1407 lines
44 KiB
HTML
Raw Normal View History

2026-11-05 15:16:24 +08:00
<!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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
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>