| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <title>门店健康诊断规则 · 当前生效(可编辑)</title>
- <style>
- *{margin:0;padding:0;box-sizing:border-box}
- body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
- background:#f8f4ea;color:#211a10;font-size:12px;line-height:1.6;-webkit-font-smoothing:antialiased}
- .toolbar{position:sticky;top:0;z-index:50;display:flex;align-items:center;gap:10px;flex-wrap:wrap;
- padding:10px 22px;background:#fffdf7;border-bottom:1px solid #ecdfc2;box-shadow:0 2px 10px rgba(120,90,20,.06)}
- .toolbar .tt{font-size:13.5px;font-weight:600;margin-right:6px}
- .toolbar .sub{font-size:10.5px;color:#93876f;margin-right:auto}
- .toolbar .sub b{color:#5c5241;font-weight:500}
- .btn{font-family:inherit;font-size:11.5px;padding:6px 12px;border-radius:7px;cursor:pointer;
- border:1px solid #ecdfc2;background:#fff;color:#5c5241;transition:.15s}
- .btn:hover{border-color:#c8952c;color:#a4761c}
- .btn.primary{background:linear-gradient(135deg,#eab94e,#d29a29 48%,#b57e15);color:#fff;border-color:transparent;font-weight:600}
- .btn.primary:hover{filter:brightness(1.05)}
- .saved{font-size:10.5px;color:#5f8d72;opacity:0;transition:opacity .3s}
- .saved.on{opacity:1}
- .wrap{padding:20px 24px 60px}
- .hd{border-bottom:1.5px solid #c8952c;padding-bottom:12px;margin-bottom:4px}
- .badge{display:inline-block;background:linear-gradient(135deg,#eab94e,#c8952c 55%,#a4761c);color:#fff;
- font-size:10px;letter-spacing:1px;padding:3px 10px;border-radius:5px;font-weight:600}
- h1{font-size:19px;font-weight:600;margin:9px 0 6px}
- .meta{font-size:11px;color:#93876f;display:flex;gap:16px;flex-wrap:wrap}
- .meta b{font-weight:500;color:#5c5241}
- .legend{display:flex;gap:14px;flex-wrap:wrap;margin:12px 0 0;font-size:10.5px;color:#93876f}
- .legend i{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:4px}
- .map{display:flex;align-items:flex-start;margin-top:16px}
- .root{flex:0 0 auto;width:140px;padding:15px 12px;background:#211a10;color:#f8f4ea;border-radius:10px;
- text-align:center;position:sticky;top:64px}
- .root b{display:block;font-size:13.5px;line-height:1.45}
- .root span{display:block;font-size:10px;color:#c9bda4;margin-top:6px;line-height:1.6}
- .spine{flex:0 0 26px;position:relative}
- .spine::before{content:"";position:absolute;left:12px;top:24px;bottom:24px;width:1.5px;background:#e0cfa6}
- .branches{flex:1;display:flex;flex-direction:column;gap:12px;min-width:0}
- .branch{position:relative;background:#fff;border:1px solid #ecdfc2;border-radius:10px;padding:12px 14px 13px 16px}
- .branch::before{content:"";position:absolute;left:-26px;top:24px;width:26px;height:1.5px;background:#e0cfa6}
- .branch::after{content:"";position:absolute;left:-5px;top:20px;width:7px;height:7px;border-radius:50%;background:#c8952c}
- .bh{display:flex;align-items:center;gap:9px;font-size:13px;font-weight:600;margin-bottom:9px}
- .bh .bar{width:3px;height:13px;border-radius:2px}
- .bh .cnt{font-size:10.5px;font-weight:400;color:#93876f;margin-left:auto}
- .bh .add{font-size:10.5px;padding:2px 9px;border-radius:5px;border:1px dashed #d9c79c;background:#fffdf7;
- color:#a4761c;cursor:pointer;font-family:inherit}
- .bh .add:hover{background:#fbf4e2;border-style:solid}
- .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:8px}
- .card{border:1px solid #f2e9d6;border-radius:8px;padding:9px 11px 10px;background:#fdfcf9;position:relative}
- .card.flagged{border-color:#e0a08c;background:#fffaf8;box-shadow:0 0 0 2px rgba(224,160,140,.16)}
- .card[data-tier="R"]{background:#fefaf8}
- .ch{display:flex;align-items:center;gap:6px;margin-bottom:6px;flex-wrap:wrap}
- .tag{font-size:9.5px;padding:1px 6px;border-radius:4px;font-weight:500;white-space:nowrap}
- .tag.R{background:#f7e0d7;color:#93401f}.tag.B{background:#f4e6cb;color:#7a5a22}
- .tag.T{background:#e8eef2;color:#4d6b7d}.tag.N{background:#efeade;color:#6b6150}
- .cid{font-size:9.5px;color:#a1957d;font-family:ui-monospace,Menlo,monospace}
- .name{font-size:12px;font-weight:600;flex:1;min-width:60px;outline:none;border-radius:3px;padding:0 2px}
- .hits{font-size:10px;color:#93876f}
- .tier-sel{border:1px solid #ece3cf;border-radius:4px;background:#fff;font-size:9.5px;color:#5c5241;
- padding:1px 2px;font-family:inherit;cursor:pointer}
- .del{position:absolute;top:5px;right:6px;width:16px;height:16px;line-height:14px;text-align:center;
- border-radius:4px;border:0;background:transparent;color:#c9bda4;cursor:pointer;font-size:13px;padding:0}
- .del:hover{background:#f7e0d7;color:#93401f}
- .rw{display:flex;gap:5px;font-size:10.5px;line-height:1.65;color:#5c5241;margin-top:3px}
- .rw .k{flex:0 0 24px;color:#a1957d;font-size:9.5px;padding-top:1px}
- .rw .v{flex:1;outline:none;border-radius:3px;padding:0 2px;white-space:pre-wrap;word-break:break-word}
- .rw .v:focus{background:#fff5dc;box-shadow:0 0 0 2px rgba(232,160,32,.25)}
- .rw.act .v{color:#4a6b52}
- .rw.ex{color:#a1957d;font-size:10px}
- .rw.ex .v{color:#8d8267;font-style:italic}
- .cf{display:flex;align-items:center;gap:8px;margin-top:7px;padding-top:6px;border-top:1px dashed #f0e8d5}
- .cf label{display:flex;align-items:center;gap:4px;font-size:10px;color:#a1957d;cursor:pointer;white-space:nowrap}
- .cf input[type=checkbox]{width:12px;height:12px;cursor:pointer;accent-color:#c8952c}
- .cf .note{flex:1;min-width:0;border:1px solid #ece3cf;border-radius:4px;padding:3px 6px;font-size:10px;
- font-family:inherit;color:#5c5241;background:#fff;outline:none}
- .cf .note:focus{border-color:#c8952c}
- .cf .note::placeholder{color:#c2b8a2}
- .tree{display:grid;grid-template-columns:1fr 1fr;gap:8px}
- .tn{border:1px solid #f2e9d6;border-radius:8px;padding:8px 11px;background:#fdfcf9;position:relative}
- .tn b{font-size:11.5px;display:block;margin-bottom:2px}
- .tn .d{font-size:10.5px;color:#7a6d57}
- .tn .ord{position:absolute;top:7px;right:9px;font-size:10px;color:#c9bda4;font-family:ui-monospace,Menlo,monospace}
- .pgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(196px,1fr));gap:7px}
- .pitem{border:1px solid #f2e9d6;border-radius:7px;padding:7px 10px;background:#fdfcf9}
- .pitem .pn{font-size:10px;color:#a1957d}
- .pitem .pv{font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;color:#211a10;outline:none}
- .pitem .pv .u{font-size:10px;font-weight:400;color:#93876f;margin-left:3px}
- .pitem .gk{font-size:9.5px;color:#c8952c;letter-spacing:.3px}
- .foot{margin-top:20px;padding-top:12px;border-top:1px solid #ecdfc2;font-size:10px;color:#a1957d;line-height:1.9}
- .foot b{color:#7a6d57;font-weight:500}
- .hintbar{margin-top:12px;padding:9px 13px;border-radius:8px;background:#fbf4e2;border:1px solid #f0e2c2;
- font-size:11px;color:#7a6a4a;line-height:1.75}
- .hintbar b{color:#a4761c}
- @page{size:A4;margin:12mm}
- @media print{
- body{background:#fff}
- .toolbar,.del,.bh .add,.cf{display:none!important}
- .root{position:static}
- .branch,.card,.tn,.pitem{break-inside:avoid}
- .wrap{padding:0}
- .card.flagged{background:#fffaf8}
- }
- @media (max-width:760px){
- .wrap{padding:14px 12px 40px}
- .map{flex-direction:column}
- .root{position:static;width:100%;margin-bottom:10px}
- .spine{display:none}
- .branch::before,.branch::after{display:none}
- .cards,.tree{grid-template-columns:1fr}
- }
- </style>
- </head>
- <body>
- <div class="toolbar">
- <span class="tt">规则全景图</span>
- <span class="sub" id="stat">共 <b id="nRules">0</b> 条规则 · 已标记 <b id="nFlag">0</b> 条待改 · <span id="nEdit">未改动</span></span>
- <span class="saved" id="savedTip">已自动保存到本机</span>
- <button class="btn" id="btnExport">导出修改(JSON)</button>
- <button class="btn" id="btnImport">导入 JSON</button>
- <button class="btn" id="btnReset">还原初始</button>
- <button class="btn primary" id="btnPrint">打印 / 存 PDF</button>
- <input type="file" id="fileInput" accept=".json,application/json" hidden>
- </div>
- <div class="wrap">
- <div class="hd">
- <span class="badge">怡呵美 · 水光会员店</span>
- <h1>门店健康诊断规则 · 当前生效(可编辑)</h1>
- <div class="meta">
- <span>数据基准 <b id="periodTxt">2026-11-02</b></span>
- <span>门店 <b id="storesTxt">1413</b> 家</span>
- <span>来源 <b>backend/rules.py 自动生成</b></span>
- </div>
- <div class="legend">
- <span><i style="background:#e0b0a0"></i>R 违规红线</span>
- <span><i style="background:#e2c88e"></i>B 考核未达标</span>
- <span><i style="background:#b9c9d4"></i>T 运营提升</span>
- <span><i style="background:#d8d0bc"></i>N 样本不足</span>
- </div>
- </div>
- <div class="hintbar">
- 所有带下划线的文字<b>可以直接点开修改</b>:改判定条件、改问题文案、改整改动作、改档位、加备注、勾「这条要改」。
- 改动会自动存在本机浏览器里,<b>导出修改(JSON)</b> 发给我,我按你的标注改 <code>rules.py</code>。
- 卡片右上角 × 可删除规则,标题栏「+ 新增规则」可加。
- </div>
- <div class="map">
- <div class="root"><b>门店健康诊断</b><span>4 层漏斗<br><i id="rootRules">—</i> 条规则<br><i id="rootParams">—</i> 个阈值<br>1 棵诊断树</span></div>
- <div class="spine"></div>
- <div class="branches" id="branches"></div>
- </div>
- <div class="foot">
- · 命中数为 <b id="periodTxt2">2026-11-02</b> 实测,同一门店可同时命中多条;诊断树只取第一条作为「主因」<br>
- · 「示例」是按当前模板用一家真实命中门店渲染出来的结果,改模板后此处不会自动变,仅供对照<br>
- · 本文件完全离线可用,不依赖任何外部资源
- </div>
- </div>
- <script>
- const INITIAL = {"period": "2026-11-02", "stores": 1413, "layers": [{"k": "traffic", "name": "① 流量层 · 能不能被看到", "color": "#6f8fa8", "rules": [{"id": "VISIT", "layer": "traffic", "tier": "T", "name": "访问过低", "cond": "访问次数 < 100(含 0 访问)", "problem": "门店曝光访问偏低", "tpl": "t_VISIT", "action": "1. 检查门店是否有违规限流\n2.点亮门店+更新AIGC素材,增加门店投稿,提升门店曝光", "hits": 161, "sample": "门店曝光访问偏低", "sampleStore": "国货护肤·怡呵美专业皮肤管理(双林中横路社区南区店)", "interlock": "", "flag": false, "note": ""}, {"id": "VIDEO", "layer": "traffic", "tier": "T", "name": "视频量少", "cond": "新发布门店关联视频数 < 10", "problem": "门店发布视频数量少", "tpl": "t_VIDEO", "action": "✅ 传素材:后台 AIGC 内容定期更 定期使用后台的AIGC素材工具,上传并更新高质量的图片与视频内容。这是吸引用户点击、提升页面转化率的基础。", "hits": 504, "sample": "门店发布视频数量少", "sampleStore": "贝丽元·怡呵美美容养生馆", "interlock": "", "flag": false, "note": ""}, {"id": "LIT", "layer": "traffic", "tier": "T", "name": "点亮偏低", "cond": "门店点亮数 < 10 且 访问 > 0", "problem": "门店点亮次数偏低", "tpl": "t_LIT", "action": "店内需放置优惠码物料,积极引导到店客户点亮门店", "hits": 1298, "sample": "门店点亮次数偏低", "sampleStore": "魅力女人·怡呵美(西村邮政店)", "interlock": "", "flag": false, "note": ""}, {"id": "RANK0", "layer": "traffic", "tier": "T", "name": "未上榜", "cond": "上榜信息为空 / 「-」,好评榜、人气榜均未进入", "problem": "未上榜(好评榜/人气榜均未进入)", "tpl": "t_RANK0", "action": "连续7天交易上翻 + 主攻好评冲本地榜单;上榜可改善平台限流", "hits": 980, "sample": "未上榜(好评榜/人气榜均未进入)", "sampleStore": "魅力女人·怡呵美(西村邮政店)", "interlock": "", "flag": false, "note": ""}, {"id": "RANK_IN", "layer": "traffic", "tier": "T", "name": "仅入围", "cond": "上榜信息只有「入围XX榜」,没有「第 N 名」", "problem": "仅入围未进榜:{榜}", "tpl": "t_RANK_IN", "action": "在已入围榜单上持续做好评与交易增量,把名次冲上去", "hits": 155, "sample": "仅入围未进榜:入围邻水县美容美体好评榜", "sampleStore": "怡呵美功效水光(城南佳景店)", "interlock": "", "flag": false, "note": ""}]}, {"k": "convert", "name": "② 转化层 · 看到了有没有下单", "color": "#a4761c", "rules": [{"id": "DEAL0", "layer": "convert", "tier": "B", "name": "零成交", "cond": "访问 > 0 且 意向成交券数 = 0", "problem": "有{v}次访问却零成交", "tpl": "t_DEAL0", "action": "检查团购是否在售、门店是否有违规限流", "hits": 54, "sample": "有11次访问却零成交", "sampleStore": "怡呵美功效水光(蓝鼎滨湖假日枫丹园店)", "interlock": "① 最上游", "flag": false, "note": ""}, {"id": "CVR", "layer": "convert", "tier": "T", "name": "转化率低", "cond": "访问 ≥ 100 且 意向成交 > 0 且 成交人数 ÷ 访问 < 10%", "problem": "成交转化率仅{v}%", "tpl": "t_CVR", "action": "提高门店基建数据,增加进店转化", "hits": 1198, "sample": "成交转化率仅2.5%", "sampleStore": "魅力女人·怡呵美(西村邮政店)", "interlock": "", "flag": false, "note": ""}]}, {"k": "fulfill", "name": "③ 履约层 · 下单了有没有核销", "color": "#c8952c", "rules": [{"id": "ZERO", "layer": "fulfill", "tier": "B", "name": "成交未核销", "cond": "门店页成交券数 > 0 且 核销券数 = 0(履约层内命中则不再判「核销率低」)", "problem": "成交{n}张券一张未核销", "tpl": "t_ZERO", "action": "触达全部已成交客户确认到店意愿,核查门店核销流程是否卡单", "hits": 213, "sample": "成交5张券一张未核销", "sampleStore": "美丽视觉·怡呵美问题性皮肤功效水光会员店", "interlock": "② 上游命中则跳过", "flag": false, "note": ""}, {"id": "HX", "layer": "fulfill", "tier": "B", "name": "核销率不达标", "cond": "门店页成交券数 ≥ 5 且 核销率 < 40%", "problem": "核销率{v}%(基准线{base}%)", "tpl": "t_HX", "action": "每天使用客资中心邀约未核销客户,设置到店核销激励", "hits": 672, "sample": "核销率3%(基准线40%)", "sampleStore": "百草芳香·怡呵美水光", "interlock": "③ 最下游", "flag": false, "note": ""}, {"id": "AMT", "layer": "fulfill", "tier": "B", "name": "客单价偏低", "cond": "核销金额 < 10,000 元(独立判定,不受核销率约束)", "problem": "当月核销{金额}元,未做大单上翻", "tpl": "t_AMT", "action": "每月至少完成1.2w大单上翻,保持金牌门店,获取平台流量激励", "hits": 1287, "sample": "当月核销270元,未做大单上翻", "sampleStore": "魅力女人·怡呵美(西村邮政店)", "interlock": "", "flag": false, "note": ""}, {"id": "PEND", "layer": "fulfill", "tier": "T", "name": "未核销积压", "cond": "估算未核销券(门店页成交券数 − 核销券数)≥ 10 张", "problem": "积压{p}张未核销券", "tpl": "t_PEND", "action": "每天使用客资中心及时邀约新增客资", "hits": 858, "sample": "积压32张未核销券", "sampleStore": "百草芳香·怡呵美水光", "interlock": "", "flag": false, "note": ""}]}, {"k": "repute", "name": "④ 口碑层 · 体验好不好", "color": "#b5563a", "rules": [{"id": "TD", "layer": "repute", "tier": "R", "name": "退单率超线", "cond": "商责退单率 > 1.27% / 3.18% / 6.35%(预警 / 一般严重 / 非常严重)", "problem": "商责退单率{v}%超{档}", "tpl": "t_TD", "action": "调整门店接待流程避免商责退单进一步加重,主动补20单核销好评,降低商责退单占比", "hits": 165, "sample": "商责退单率2.50%超预警线", "sampleStore": "百草芳香·怡呵美水光", "interlock": "", "flag": false, "note": ""}, {"id": "TS", "layer": "repute", "tier": "R", "name": "投诉率超线", "cond": "投诉率 > 0.11% / 0.28% / 0.56%(三级)", "problem": "投诉率{v}%超{档}", "tpl": "t_TS", "action": "调整门店接待流程避免客诉进一步加重", "hits": 9, "sample": "投诉率2.70%超非常严重线", "sampleStore": "怡呵美全效水光中心(亨达商业广场店)", "interlock": "", "flag": false, "note": ""}, {"id": "CP", "layer": "repute", "tier": "R", "name": "差评率超线", "cond": "新增评价 ≥ 10 → 新增口径;否则累计评价 ≥ 20 → 累计口径;都不足仍判定但最高只到预警线。超 0.66% / 1.66% / 3.31%", "problem": "差评率({口径}口径){v}%超{档}", "tpl": "t_CP", "action": "逐条处理近30天差评,主攻优质深度好评(避免无效评价)", "hits": 350, "sample": "差评率(累计口径)1.96%超一般严重线", "sampleStore": "哈尼罕·怡呵美植物养发馆(嘉祥路店)", "interlock": "", "flag": false, "note": ""}, {"id": "SCORE", "layer": "repute", "tier": "B", "name": "评分不达标", "cond": "评分 < 4.3(0 分按「未开分」处理,文案与动作单独走)", "problem": "评分{v}(基准线{base})-低于平台准入", "tpl": "t_SCORE", "action": "优先引导带图深度好评,需先将门店评分提升至{base}以上", "hits": 327, "sample": "评分4.1(基准线4.3)-低于平台准入", "sampleStore": "怡呵美·韩国维娜(政府路店)", "interlock": "", "flag": false, "note": ""}, {"id": "REPLY", "layer": "repute", "tier": "B", "name": "回复率偏低", "cond": "新增评价 ≥ 1 且 评价回复率 < 50%", "problem": "新增{新增}条评价,回复率{v}%偏低", "tpl": "t_REPLY", "action": "安排时间定时回复新增评价,差评24小时内及时处理", "hits": 834, "sample": "新增5条评价,回复率0%偏低", "sampleStore": "魅力女人·怡呵美(西村邮政店)", "interlock": "", "flag": false, "note": ""}, {"id": "RVOL", "layer": "repute", "tier": "T", "name": "新增评价少", "cond": "新增评价数 < 10", "problem": "近30天新增评价少", "tpl": "t_RVOL", "action": "每月保持不低于15条新增优质评价", "hits": 1164, "sample": "近30天新增评价少", "sampleStore": "魅力女人·怡呵美(西村邮政店)", "interlock": "", "flag": false, "note": ""}]}], "params": [{"group": "门店评分", "key": "score_base", "label": "考核基准", "unit": "分", "value": "4.3"}, {"group": "门店评分", "key": "score_target", "label": "目标值", "unit": "分", "value": "4.5"}, {"group": "核销率", "key": "hx_rate_base", "label": "考核基准", "unit": "%", "value": "40"}, {"group": "核销率", "key": "hx_rate_target", "label": "目标值", "unit": "%", "value": "0.6"}, {"group": "核销金额", "key": "hx_amt_base", "label": "考核基准", "unit": "元", "value": "10000"}, {"group": "商责退单率", "key": "tuDan", "label": "三级违规线", "unit": "%", "value": "1.27 / 3.18 / 6.35"}, {"group": "投诉率", "key": "touSu", "label": "三级违规线", "unit": "%", "value": "0.11 / 0.28 / 0.56"}, {"group": "差评率", "key": "chaPing", "label": "三级违规线", "unit": "%", "value": "0.66 / 1.66 / 3.31"}, {"group": "评价回复率", "key": "reply_base", "label": "最低要求", "unit": "%", "value": "50"}, {"group": "访问量", "key": "visit_low", "label": "过低线", "unit": "次", "value": "100"}, {"group": "成交转化率", "key": "cvr_low", "label": "过低线", "unit": "%", "value": "10"}, {"group": "内容与评价量", "key": "video_low", "label": "视频数下限", "unit": "条", "value": "10"}, {"group": "内容与评价量", "key": "lit_low", "label": "点亮数下限", "unit": "条", "value": "10"}, {"group": "内容与评价量", "key": "new_review_low", "label": "新增评价数下限", "unit": "条", "value": "10"}, {"group": "未核销积压", "key": "pend_high", "label": "提醒线", "unit": "张", "value": "10"}, {"group": "样本量门槛", "key": "cp_min_new", "label": "差评率·新增最小样本", "unit": "条", "value": "10"}, {"group": "样本量门槛", "key": "cp_min_total", "label": "差评率·累计最小样本", "unit": "条", "value": "20"}, {"group": "样本量门槛", "key": "reply_min_new", "label": "回复率最小新增评价", "unit": "条", "value": "1"}, {"group": "样本量门槛", "key": "hx_min_deal", "label": "核销率最小成交券", "unit": "张", "value": "5"}, {"group": "样本量门槛", "key": "amt_min_coupons", "label": "客单价最小核销券", "unit": "张", "value": "5"}], "tree": [{"n": "任一 R 级违规", "d": "退单率 / 投诉率 / 差评率命中三级线", "who": "取其中级别最高的一条"}, {"n": "VISIT · 访问过低", "d": "访问次数 < 100(含 0 访问)", "who": ""}, {"n": "DEAL0 · 零成交", "d": "访问 > 0 且 意向成交券数 = 0", "who": ""}, {"n": "CVR · 转化率低", "d": "访问 ≥ 100 且 意向成交 > 0 且 成交人数 ÷ 访问 < 10%", "who": ""}, {"n": "ZERO · 成交未核销", "d": "门店页成交券数 > 0 且 核销券数 = 0(履约层内命中则不再判「核销率低」)", "who": ""}, {"n": "HX · 核销率不达标", "d": "门店页成交券数 ≥ 5 且 核销率 < 40%", "who": ""}, {"n": "AMT · 客单价偏低", "d": "核销金额 < 10,000 元(独立判定,不受核销率约束)", "who": ""}, {"n": "SCORE · 评分不达标", "d": "评分 < 4.3(0 分按「未开分」处理,文案与动作单独走)", "who": ""}, {"n": "REPLY · 回复率偏低", "d": "新增评价 ≥ 1 且 评价回复率 < 50%", "who": ""}, {"n": "RVOL · 新增评价少", "d": "新增评价数 < 10", "who": ""}, {"n": "VIDEO · 视频量少", "d": "新发布门店关联视频数 < 10", "who": ""}, {"n": "LIT · 点亮偏低", "d": "门店点亮数 < 10 且 访问 > 0", "who": ""}, {"n": "PEND · 未核销积压", "d": "估算未核销券(门店页成交券数 − 核销券数)≥ 10 张", "who": ""}, {"n": "RANK0 · 未上榜", "d": "上榜信息为空 / 「-」,好评榜、人气榜均未进入", "who": ""}, {"n": "RANK_IN · 仅入围", "d": "上榜信息只有「入围XX榜」,没有「第 N 名」", "who": ""}], "principles": [{"name": "数据可信才判", "cond": "样本不足只标「样本不足」,差评率仍判但封顶预警线"}, {"name": "根因去重", "cond": "零成交 > 成交未核销 > 核销率低 > 客单价低,只报最上游一条"}, {"name": "五档分级", "cond": "R 违规红线 / B 考核未达标 / T 运营提升 / N 样本不足"}, {"name": "动作要可执行", "cond": "每家最多 3 条,绑定该店真实数字"}, {"name": "阈值可调", "cond": "所有判定参数都在【指标配置】里可改,不用动代码"}]};
- const STORE_KEY = 'yhm_rules_map_v2';
- let DATA = null;
- function clone(o){ return JSON.parse(JSON.stringify(o)); }
- function esc(s){ return String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
- /* ---------- 持久化 ---------- */
- let saveTimer = null;
- function save(){
- localStorage.setItem(STORE_KEY, JSON.stringify(DATA));
- const tip = document.getElementById('savedTip');
- tip.classList.add('on');
- clearTimeout(saveTimer);
- saveTimer = setTimeout(() => tip.classList.remove('on'), 1200);
- }
- function saveSoon(){ clearTimeout(saveTimer); saveTimer = setTimeout(save, 400); }
- function load(){
- try {
- const raw = localStorage.getItem(STORE_KEY);
- if (raw) { DATA = JSON.parse(raw); return; }
- } catch (e) {}
- DATA = clone(INITIAL);
- }
- /* ---------- 修改统计 ---------- */
- function printOf(d){
- const out = [];
- d.layers.forEach(L => L.rules.forEach(r => {
- out.push([r.id, r.tier, r.name, r.cond, r.problem, r.action, r.note]);
- }));
- d.params.forEach(p => out.push([p.key, p.value]));
- return JSON.stringify(out);
- }
- const BASE_PRINT = printOf(INITIAL);
- function updateStat(){
- let n = 0, flag = 0;
- DATA.layers.forEach(L => L.rules.forEach(r => { n++; if (r.flag) flag++; }));
- document.getElementById('nRules').textContent = n;
- document.getElementById('nFlag').textContent = flag;
- const touched = printOf(DATA) !== BASE_PRINT;
- const el = document.getElementById('nEdit');
- el.textContent = touched ? '内容已改动' : '未改动';
- el.style.color = touched ? '#b5563a' : '';
- }
- /* ---------- 渲染 ---------- */
- function tierOptions(cur){
- return ['R','B','T','N'].map(t =>
- '<option value="' + t + '"' + (t === cur ? ' selected' : '') + '>' + t + '</option>').join('');
- }
- function cardHtml(r){
- return '<div class="card' + (r.flag ? ' flagged' : '') + '" data-id="' + esc(r.id) + '" data-tier="' + esc(r.tier) + '">' +
- '<button class="del" data-act="del" title="删除这条规则">×</button>' +
- '<div class="ch">' +
- '<span class="tag ' + esc(r.tier) + '">' + (r.tier) + '</span>' +
- '<span class="cid">' + esc(r.id) + '</span>' +
- '<span class="name" contenteditable="true" data-f="name">' + esc(r.name) + '</span>' +
- '<select class="tier-sel" data-f="tier" title="改档位">' + tierOptions(r.tier) + '</select>' +
- '<span class="hits">' + (r.hits || 0) + ' 家</span>' +
- '</div>' +
- (r.interlock ? '<div class="rw"><span class="k">互斥</span><span class="v" style="color:#a4761c">' + esc(r.interlock) + '</span></div>' : '') +
- '<div class="rw"><span class="k">判定</span><span class="v" contenteditable="true" data-f="cond">' + esc(r.cond) + '</span></div>' +
- '<div class="rw"><span class="k">问题</span><span class="v" contenteditable="true" data-f="problem">' + esc(r.problem) + '</span></div>' +
- '<div class="rw act"><span class="k">动作</span><span class="v" contenteditable="true" data-f="action">' + esc(r.action) + '</span></div>' +
- (r.sample ? '<div class="rw ex"><span class="k">示例</span><span class="v">' + esc(r.sample) + '</span></div>' : '') +
- '<div class="cf">' +
- '<label><input type="checkbox" data-f="flag"' + (r.flag ? ' checked' : '') + '>这条要改</label>' +
- '<input class="note" data-f="note" placeholder="备注:哪里不对 / 想改成什么" value="' + esc(r.note) + '">' +
- '</div>' +
- '</div>';
- }
- function render(){
- const box = document.getElementById('branches');
- let h = '';
- // 判定原则
- h += '<div class="branch"><div class="bh"><span class="bar" style="background:#8a6a2f"></span>判定原则<span class="cnt">' +
- DATA.principles.length + ' 条</span></div><div class="cards">' +
- DATA.principles.map(p => '<div class="card"><div class="ch"><span class="tag T">原则</span>' +
- '<span class="name" contenteditable="true" data-pf="name" data-pid="' + esc(p.name) + '">' + esc(p.name) + '</span></div>' +
- '<div class="rw"><span class="k">说明</span><span class="v" contenteditable="true" data-pf="cond" data-pid="' + esc(p.name) + '">' +
- esc(p.cond) + '</span></div></div>').join('') + '</div></div>';
- // 四层
- DATA.layers.forEach(L => {
- h += '<div class="branch"><div class="bh"><span class="bar" style="background:' + L.color + '"></span>' +
- L.name + '<span class="cnt">' + L.rules.length + ' 条规则</span>' +
- '<button class="add" data-act="add" data-layer="' + L.k + '">+ 新增规则</button></div>' +
- '<div class="cards">' + L.rules.map(cardHtml).join('') + '</div></div>';
- });
- // 可调阈值
- h += '<div class="branch"><div class="bh"><span class="bar" style="background:#5f8d72"></span>可调阈值' +
- '<span class="cnt">' + DATA.params.length + ' 个 · 在【指标配置】改</span></div><div class="pgrid">' +
- DATA.params.map(p => '<div class="pitem" data-key="' + esc(p.key) + '">' +
- '<div class="gk">' + esc(p.group) + '</div>' +
- '<div class="pn">' + esc(p.label) + '</div>' +
- '<div class="pv" contenteditable="true" data-pf="value" data-pid="' + esc(p.key) + '">' + esc(p.value) +
- '<span class="u">' + esc(p.unit) + '</span></div></div>').join('') + '</div></div>';
- // 诊断树
- h += '<div class="branch"><div class="bh"><span class="bar" style="background:#5c5241"></span>诊断树 · 主因怎么选(从上往下取第一个命中)' +
- '<span class="cnt">' + DATA.tree.length + ' 步</span></div><div class="tree">' +
- DATA.tree.map((t, i) => '<div class="tn"><span class="ord">' + String(i + 1).padStart(2, '0') + '</span>' +
- '<b>' + esc(t.n) + '</b><div class="d">' + esc(t.d) + '</div>' +
- (t.who ? '<div class="d" style="color:#a4761c">→ ' + esc(t.who) + '</div>' : '') + '</div>').join('') +
- '</div></div>';
- box.innerHTML = h;
- const nr = DATA.layers.reduce((n, L) => n + L.rules.length, 0);
- const rr = document.getElementById('rootRules');
- const rp = document.getElementById('rootParams');
- if (rr) rr.textContent = nr;
- if (rp) rp.textContent = DATA.params.length;
- updateStat();
- }
- /* ---------- 事件 ---------- */
- function findRule(id){
- for (const L of DATA.layers) { const r = L.rules.find(x => x.id === id); if (r) return r; }
- return null;
- }
- document.addEventListener('input', e => {
- const el = e.target;
- // 规则文本
- const card = el.closest && el.closest('.card[data-id]');
- if (card && el.dataset.f) {
- const r = findRule(card.dataset.id);
- if (r) {
- const f = el.dataset.f;
- if (f === 'flag') { r.flag = el.checked; card.classList.toggle('flagged', r.flag); }
- else if (f === 'tier') { r.tier = el.value; }
- else r[f] = (el.tagName === 'INPUT' || el.tagName === 'SELECT') ? el.value : el.textContent;
- saveSoon(); updateStat();
- return;
- }
- }
- // 原则 / 阈值
- if (el.dataset.pf) {
- if (el.dataset.pf === 'value') {
- const p = DATA.params.find(x => x.key === el.dataset.pid);
- if (p) { p.value = el.textContent.replace(/\s*[^\d.\/\s-]+$/, '').trim(); saveSoon(); updateStat(); }
- } else {
- const p = DATA.principles.find(x => x.name === el.dataset.pid);
- if (p) { p[el.dataset.pf] = el.textContent; saveSoon(); updateStat(); }
- }
- }
- });
- document.addEventListener('change', e => {
- if (e.target.dataset && e.target.dataset.f === 'tier') {
- render(); save();
- }
- });
- document.addEventListener('click', e => {
- const btn = e.target.closest && e.target.closest('[data-act]');
- if (!btn) return;
- const act = btn.dataset.act;
- if (act === 'del') {
- const card = btn.closest('.card[data-id]');
- const id = card.dataset.id;
- if (!confirm('删除规则「' + id + '」?')) return;
- DATA.layers.forEach(L => { L.rules = L.rules.filter(r => r.id !== id); });
- render(); save();
- }
- if (act === 'add') {
- const layer = btn.dataset.layer;
- const L = DATA.layers.find(x => x.k === layer);
- let n = 1, id;
- do { id = 'NEW-' + n++; } while (findRule(id));
- L.rules.push({ id, layer, tier: 'T', name: '新规则', cond: '在这里写判定条件',
- problem: '在这里写问题文案', action: '在这里写整改动作', hits: 0,
- sample: '', sampleStore: '', interlock: '', flag: true, note: '' });
- render(); save();
- }
- });
- /* 导出 / 导入 / 还原 */
- document.getElementById('btnExport').onclick = () => {
- const blob = new Blob([JSON.stringify(DATA, null, 2)], { type: 'application/json;charset=utf-8' });
- const a = document.createElement('a');
- a.href = URL.createObjectURL(blob);
- a.download = '规则修改_' + new Date().toISOString().slice(0, 10) + '.json';
- document.body.appendChild(a); a.click(); a.remove();
- setTimeout(() => URL.revokeObjectURL(a.href), 3000);
- };
- document.getElementById('btnImport').onclick = () => document.getElementById('fileInput').click();
- document.getElementById('fileInput').onchange = e => {
- const f = e.target.files[0]; if (!f) return;
- const fr = new FileReader();
- fr.onload = () => {
- try { DATA = JSON.parse(fr.result); render(); save(); alert('已导入'); }
- catch (err) { alert('JSON 解析失败:' + err.message); }
- };
- fr.readAsText(f);
- e.target.value = '';
- };
- document.getElementById('btnReset').onclick = () => {
- if (!confirm('还原成代码里的当前规则?你的修改会丢失。')) return;
- DATA = clone(INITIAL); render(); save();
- };
- document.getElementById('btnPrint').onclick = () => window.print();
- load();
- render();
- </script>
- </body>
- </html>
|