Files
ObsidianHome/小鱼/小鱼运营决策助手_v0.1.html
2026-11-05 15:16:24 +08:00

1408 lines
44 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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>