# -*- coding: utf-8 -*-
"""生成南部大区城市经营看板单文件 HTML（ECharts）。"""
import json

data = json.load(open("南部大区城市看板/dashboard_data.json", encoding="utf-8"))
DATA_JSON = json.dumps(data, ensure_ascii=False)

HTML = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>南部大区城市经营看板 · 2026年7月</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
:root{--bg:#f5f7fa;--card:#fff;--ink:#1f2d3d;--sub:#8492a6;--line:#e4e9f0;--blue:#1f6fe0;--green:#18a058;--red:#d03050;--orange:#f0a020;}
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:"PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--ink);font-size:13px;}
.wrap{max-width:1440px;margin:0 auto;padding:16px;}
header{background:linear-gradient(120deg,#0b2a5b,#1f6fe0);color:#fff;border-radius:12px;padding:18px 24px;margin-bottom:14px;}
header h1{font-size:20px;font-weight:700;}
header .meta{margin-top:6px;font-size:12px;opacity:.85;}
header .meta span{margin-right:16px;}
.cards{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:14px;}
.card{background:var(--card);border-radius:10px;padding:12px 14px;box-shadow:0 1px 3px rgba(16,42,88,.06);}
.card .k{color:var(--sub);font-size:12px;margin-bottom:6px;}
.card .v{font-size:20px;font-weight:700;}
.card .d{font-size:11px;margin-top:4px;}
.up{color:var(--green);} .down{color:var(--red);} .flat{color:var(--sub);}
.panel{background:var(--card);border-radius:10px;padding:14px 16px;margin-bottom:14px;box-shadow:0 1px 3px rgba(16,42,88,.06);}
.panel h2{font-size:15px;margin-bottom:4px;}
.panel .note{color:var(--sub);font-size:11px;margin-bottom:8px;}
.toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:8px 0;}
.btn{border:1px solid var(--line);background:#fff;border-radius:16px;padding:4px 12px;cursor:pointer;font-size:12px;color:var(--ink);}
.btn.on{background:var(--blue);border-color:var(--blue);color:#fff;}
input[type=text]{border:1px solid var(--line);border-radius:16px;padding:4px 12px;font-size:12px;width:160px;outline:none;}
table{width:100%;border-collapse:collapse;font-size:12px;}
th{background:#f0f4f9;padding:7px 8px;text-align:right;cursor:pointer;user-select:none;white-space:nowrap;border-bottom:2px solid var(--line);}
th.l,td.l{text-align:left;}
td{padding:6px 8px;text-align:right;border-bottom:1px solid var(--line);white-space:nowrap;}
tr:hover td{background:#f6f9ff;}
.tag{display:inline-block;border-radius:4px;padding:1px 6px;font-size:11px;color:#fff;}
.tag.zy{background:#1f6fe0;} .tag.ly{background:#f0a020;}
.gm-bad{background:#fde8ec;color:#d03050;} .gm-warn{background:#fff4e0;color:#c07800;}
.gm-ok{background:#e8f7ee;color:#18a058;} .gm-good{background:#d2f0dd;color:#0a7a40;}
.gm{display:inline-block;border-radius:4px;padding:1px 6px;min-width:52px;text-align:center;}
.chart{width:100%;height:420px;}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
.grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;}
.rank{background:var(--card);border-radius:10px;padding:12px 14px;box-shadow:0 1px 3px rgba(16,42,88,.06);}
.rank h3{font-size:13px;margin-bottom:8px;}
.rank li{display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px dashed var(--line);list-style:none;}
.rank .no{color:var(--sub);width:20px;}
footer{color:var(--sub);font-size:11px;padding:10px 4px 20px;line-height:1.7;}
@media(max-width:1100px){.cards{grid-template-columns:repeat(3,1fr);}.grid2,.grid3{grid-template-columns:1fr;}}
</style>
</head>
<body>
<div class="wrap">
<header>
  <h1>南部大区城市经营看板</h1>
  <div class="meta">
    <span>📊 数据源：伽利略 · 经营管理报表_核心指标（vizId 17040 · 数据集 35003 细分管报）</span>
    <span>🗓 主视图：2026年7月（自然月）</span>
    <span>📈 趋势：2026年5月–8月（8月为 MTD 截至 08.17）</span>
    <span>🏙 62 城 · 直营 12 / 联营 50</span>
  </div>
</header>

<div class="cards" id="cards"></div>

<div class="grid2">
  <div class="panel">
    <h2>城市格局：收入 × 毛利率</h2>
    <div class="note">气泡大小=换电用户数(含单次)；虚线为收入/毛利率中位数。右上=支柱城市，左上=高质量小城，右下=增收不增利重点关注。</div>
    <div id="scatter" class="chart"></div>
  </div>
  <div class="panel">
    <h2>成本费率结构 · 收入 Top15 城市</h2>
    <div class="note">各项成本费率（占税后收入比）堆叠，未含销售业务。</div>
    <div id="costbar" class="chart"></div>
  </div>
</div>

<div class="panel">
  <h2>城市经营总表 · 2026年7月</h2>
  <div class="note">点击表头排序；毛利率配色：<span class="gm gm-bad">&lt;15%</span> <span class="gm gm-warn">15–25%</span> <span class="gm gm-ok">25–35%</span> <span class="gm gm-good">&gt;35%</span></div>
  <div class="toolbar">
    <button class="btn on" data-type="all">全部 (62)</button>
    <button class="btn" data-type="直营">直营 (12)</button>
    <button class="btn" data-type="联营">联营 (50)</button>
    <input type="text" id="search" placeholder="🔍 搜索城市…">
    <span id="suminfo" style="color:var(--sub);font-size:12px;"></span>
  </div>
  <div style="overflow-x:auto;">
  <table id="citytable">
    <thead><tr>
      <th class="l">城市</th><th class="l">模式</th>
      <th data-k="users">换电用户(含单次)</th><th data-k="users_mom">用户环比</th>
      <th data-k="rev">税后收入(万)</th><th data-k="rev_mom">收入环比</th>
      <th data-k="gp">运营毛利(万)</th><th data-k="gpm">毛利率</th><th data-k="gpm_mom">毛利率Δpp</th>
      <th data-k="gp_user">单用户毛利</th><th data-k="kedanjia">客单价</th>
      <th data-k="kwh_user">日均用电</th><th data-k="r_dianfei">电费率</th><th data-k="r_yingxiao">营销费率</th>
    </tr></thead>
    <tbody></tbody>
  </table>
  </div>
</div>

<div class="panel">
  <h2>城市趋势 · 2026年5月 → 8月MTD</h2>
  <div class="note">点选城市对比（最多 6 个），含大区合计基准线。</div>
  <div class="toolbar" id="metricbar">
    <button class="btn on" data-m="rev">税后收入(万)</button>
    <button class="btn" data-m="gpm">毛利率</button>
    <button class="btn" data-m="users">换电用户数(含单次)</button>
    <button class="btn" data-m="kedanjia">客单价</button>
  </div>
  <div class="toolbar" id="citybar"></div>
  <div id="trend" class="chart"></div>
</div>

<div class="grid3" id="ranks"></div>

<footer>
口径说明：收入=税后收入(不含销售)；毛利=换电业务运营毛利；用户=换电用户数(含单次)月末值；费率=各项成本/税后收入。
数据取自极光看板 17040「日期(月)_按城市」组件（daqu_name=南部大区），城市加总与大区聚合一致（7月 8,956万）。
8月为月度累计 MTD（截至 08.17），与完整月份不可直接对比。生成时间：2026-08-18 · 唐雯经营分析助手
</footer>
</div>
<script>
const DATA = __DATA__;
const fmtW = v => v==null?'–':(v/1e4).toFixed(1);
const fmtP = (v,d=1) => v==null?'–':(v*100).toFixed(d)+'%';
const fmtN = (v,d=0) => v==null?'–':v.toLocaleString('zh-CN',{maximumFractionDigits:d,minimumFractionDigits:0});
const fmtD = (v,d=1) => v==null?'–':Number(v).toFixed(d);
function momHtml(v,isPp){
  if(v==null) return '<span class="flat">–</span>';
  const s = isPp ? (v>0?'+':'')+(v*100).toFixed(1)+'pp' : (v>0?'+':'')+(v*100).toFixed(1)+'%';
  return `<span class="${v>0?'up':v<0?'down':'flat'}">${v>0?'▲':v<0?'▼':'■'} ${s}</span>`;
}
function gmCls(v){return v==null?'':v<0.15?'gm-bad':v<0.25?'gm-warn':v<0.35?'gm-ok':'gm-good';}

// ---- 汇总卡片 ----
const c7=DATA.cards['202607'], c6=DATA.cards['202606'], c8=DATA.cards['202608'];
function dlt(a,b,isPp){ if(a==null||b==null||b===0) return '<span class="flat">–</span>';
  const v=isPp?a-b:(a-b)/b; return momHtml(v,isPp);}
document.getElementById('cards').innerHTML = [
  ['税后收入(不含销售)', fmtW(c7.rev)+'万', dlt(c7.rev,c6.rev)],
  ['运营毛利', fmtW(c7.gp)+'万', dlt(c7.gp,c6.gp)],
  ['毛利率', fmtP(c7.gpm), dlt(c7.gpm,c6.gpm,true)],
  ['换电用户(含单次)月末', fmtN(c7.users_ym), dlt(c7.users_ym,c6.users_ym)],
  ['客单价', '¥'+fmtD(c7.kedanjia), dlt(c7.kedanjia,c6.kedanjia)],
  ['单用户毛利', '¥'+fmtD(c7.gp_user), dlt(c7.gp_user,c6.gp_user)],
].map(([k,v,d])=>`<div class="card"><div class="k">${k} · 环比6月</div><div class="v">${v}</div><div class="d">${d}</div></div>`).join('')
+ `<div class="card" style="grid-column:span 6;background:#eef4ff;"><div class="k">8月 MTD（截至 08.17，供参考）</div><div style="display:flex;gap:34px;flex-wrap:wrap;margin-top:4px;">
<span>收入 <b>${fmtW(c8.rev)}万</b></span><span>毛利 <b>${fmtW(c8.gp)}万</b></span><span>毛利率 <b>${fmtP(c8.gpm)}</b></span>
<span>换电用户(月末口径) <b>${fmtN(c8.users_ym)}</b></span><span>客单价 <b>¥${fmtD(c8.kedanjia)}</b></span>
<span>日均用电/用户 <b>${fmtD(c8.kwh_user,2)}度</b></span><span>电池配比 <b>${fmtD(c8.peibi,2)}</b></span><span>运营柜数 <b>${fmtN(c8.cabt_num)}</b></span></div></div>`;

// ---- 散点 ----
const sc = echarts.init(document.getElementById('scatter'));
const pts = t => DATA.city_rows.filter(r=>r.type===t&&r.rev!=null&&r.rev>=10000).map(r=>[r.rev/1e4,(r.gpm??0)*100,r.users,r.city,r.gp]);
const medX = (()=>{const a=DATA.city_rows.map(r=>r.rev||0).sort((x,y)=>x-y);return a[Math.floor(a.length/2)]/1e4;})();
const medY = (()=>{const a=DATA.city_rows.filter(r=>r.gpm!=null).map(r=>r.gpm).sort((x,y)=>x-y);return a[Math.floor(a.length/2)]*100;})();
sc.setOption({
 tooltip:{formatter:p=>`<b>${p.data[3]}</b>（${p.seriesName}）<br>收入 ${p.data[0].toFixed(1)}万 · 毛利率 ${p.data[1].toFixed(1)}%<br>用户 ${fmtN(p.data[2])} · 毛利 ${fmtW(p.data[4])}万`},
 legend:{data:['直营','联营'],top:0},
 grid:{left:50,right:20,top:34,bottom:40},
 xAxis:{name:'税后收入(万)',type:'log',min:1,nameLocation:'middle',nameGap:26},
 yAxis:{name:'毛利率%',min:0,max:70},
 series:[
  {name:'直营',type:'scatter',data:pts('直营'),symbolSize:d=>Math.max(6,Math.sqrt(d[2])/6),itemStyle:{color:'#1f6fe0',opacity:.75},
   markLine:{silent:true,symbol:'none',lineStyle:{type:'dashed',color:'#aab4c2'},label:{show:false},data:[{xAxis:medX},{yAxis:medY}]}},
  {name:'联营',type:'scatter',data:pts('联营'),symbolSize:d=>Math.max(6,Math.sqrt(d[2])/6),itemStyle:{color:'#f0a020',opacity:.75}}
 ]});

// ---- 成本结构 ----
const cb = echarts.init(document.getElementById('costbar'));
const top15 = DATA.city_rows.slice(0,15).reverse();
const costKeys=[['r_dianfei','电费','#d03050'],['r_btrydep','电池折旧','#f0a020'],['r_cabtdep','电柜折旧','#e6c345'],
 ['r_zujin','租金','#8a5cd6'],['r_renli','全职人力','#1f6fe0'],['r_jianzhi','兼职劳务','#5a9cf8'],
 ['r_daiyunying','代运营','#18a058'],['r_yingxiao','营销','#f07850'],['r_qita','其他','#9aa5b1']];
cb.setOption({
 tooltip:{trigger:'axis',axisPointer:{type:'shadow'},valueFormatter:v=>v==null?'–':v.toFixed(1)+'%',
  formatter:ps=>{const r=top15[ps[0].dataIndex];return `<b>${r.city}</b> 收入${fmtW(r.rev)}万 毛利率${fmtP(r.gpm)}<br>`+ps.filter(p=>p.value>0).map(p=>`${p.marker}${p.seriesName} ${p.value.toFixed(1)}%`).join('<br>');}},
 legend:{top:0,textStyle:{fontSize:11}},
 grid:{left:70,right:30,top:40,bottom:10},
 xAxis:{type:'value',max:100,axisLabel:{formatter:'{value}%'}},
 yAxis:{type:'category',data:top15.map(r=>r.city),axisLabel:{fontSize:11}},
 series:costKeys.map(([k,n,c])=>({name:n,type:'bar',stack:'s',barWidth:14,itemStyle:{color:c},
   data:top15.map(r=>r[k]==null?0:+(r[k]*100).toFixed(2))}))
});

// ---- 城市表 ----
let rows=[...DATA.city_rows], sortK='rev', sortDir=-1, fType='all', fQ='';
function renderTable(){
  let rs=rows.filter(r=>(fType==='all'||r.type===fType)&&(!fQ||r.city.includes(fQ)));
  rs.sort((a,b)=>{const x=a[sortK],y=b[sortK];if(x==null)return 1;if(y==null)return -1;return (x-y)*sortDir;});
  document.querySelector('#citytable tbody').innerHTML = rs.map(r=>`<tr>
   <td class="l"><b>${r.city}</b></td>
   <td class="l"><span class="tag ${r.type==='直营'?'zy':'ly'}">${r.type}</span></td>
   <td>${fmtN(r.users)}</td><td>${momHtml(r.users_mom)}</td>
   <td><b>${fmtW(r.rev)}</b></td><td>${momHtml(r.rev_mom)}</td>
   <td>${fmtW(r.gp)}</td>
   <td><span class="gm ${gmCls(r.gpm)}">${fmtP(r.gpm)}</span></td><td>${momHtml(r.gpm_mom,true)}</td>
   <td>${fmtD(r.gp_user)}</td><td>${fmtD(r.kedanjia)}</td>
   <td>${fmtD(r.kwh_user,2)}</td><td>${fmtP(r.r_dianfei)}</td><td>${fmtP(r.r_yingxiao)}</td></tr>`).join('');
  const rev=rs.reduce((s,r)=>s+(r.rev||0),0), gp=rs.reduce((s,r)=>s+(r.gp||0),0);
  document.getElementById('suminfo').textContent=`筛选后 ${rs.length} 城 · 收入合计 ${fmtW(rev)}万 · 毛利 ${fmtW(gp)}万 · 综合毛利率 ${rev?fmtP(gp/rev):'–'}`;
}
document.querySelectorAll('#citytable th[data-k]').forEach(th=>th.onclick=()=>{
  const k=th.dataset.k; if(sortK===k)sortDir*=-1; else{sortK=k;sortDir=-1;} renderTable();});
document.querySelectorAll('.toolbar .btn[data-type]').forEach(b=>b.onclick=()=>{
  document.querySelectorAll('.btn[data-type]').forEach(x=>x.classList.remove('on')); b.classList.add('on'); fType=b.dataset.type; renderTable();});
document.getElementById('search').oninput=e=>{fQ=e.target.value.trim();renderTable();};
renderTable();

// ---- 趋势 ----
const tr = echarts.init(document.getElementById('trend'));
let metric='rev'; const selCities=new Set(['广州市','深圳市','东莞市','佛山市']);
const palette=['#1f6fe0','#d03050','#18a058','#f0a020','#8a5cd6','#12b8c8'];
const allCities=DATA.city_rows.map(r=>r.city);
function renderCityBar(){
  document.getElementById('citybar').innerHTML = allCities.map(c=>
    `<button class="btn ${selCities.has(c)?'on':''}" data-c="${c}">${c}</button>`).join('');
  document.querySelectorAll('#citybar .btn').forEach(b=>b.onclick=()=>{
    const c=b.dataset.c;
    if(selCities.has(c)) selCities.delete(c); else if(selCities.size<6) selCities.add(c);
    renderCityBar(); renderTrend();});
}
const mFmt={rev:v=>fmtW(v)+'万',gpm:v=>fmtP(v),users:v=>fmtN(v),kedanjia:v=>'¥'+fmtD(v)};
function renderTrend(){
  const ser=[{name:'南部大区(合计)',type:'line',lineStyle:{type:'dashed',width:2},symbol:'diamond',symbolSize:8,
    data:DATA.months.map(m=>{const v=DATA.agg_trend[m][metric];return metric==='gpm'&&v!=null?+(v*100).toFixed(2):metric==='rev'&&v!=null?+(v/1e4).toFixed(1):v;})}];
  [...selCities].forEach((c,i)=>{
    ser.push({name:c,type:'line',symbolSize:6,lineStyle:{width:2},itemStyle:{color:palette[i%6]},
      data:DATA.months.map(m=>{const v=(DATA.trend[c]||{})[m]?.[metric];return v==null?null:metric==='gpm'?+(v*100).toFixed(2):metric==='rev'?+(v/1e4).toFixed(1):+Number(v).toFixed(2);})});
  });
  tr.setOption({
   tooltip:{trigger:'axis',valueFormatter:v=>v==null?'–':metric==='gpm'?v+'%':mFmt[metric](metric==='rev'?v*1e4:v)},
   legend:{top:0,textStyle:{fontSize:11}},
   grid:{left:60,right:30,top:40,bottom:30},
   xAxis:{type:'category',data:DATA.months.map(m=>DATA.mlabel[m])},
   yAxis:metric==='gpm'?{axisLabel:{formatter:'{value}%'}}:{},
   series:ser}, true);
}
document.querySelectorAll('#metricbar .btn').forEach(b=>b.onclick=()=>{
  document.querySelectorAll('#metricbar .btn').forEach(x=>x.classList.remove('on')); b.classList.add('on'); metric=b.dataset.m; renderTrend();});
renderCityBar(); renderTrend();

// ---- 榜单 ----
const big=DATA.city_rows.filter(r=>(r.rev||0)>=100000);
const rankList=(title,rs,valFn,fmt)=>`<div class="rank"><h3>${title}</h3><ul>${rs.map((r,i)=>
  `<li><span><span class="no">${i+1}</span>${r.city} <span class="tag ${r.type==='直营'?'zy':'ly'}">${r.type}</span></span><b>${fmt(valFn(r))}</b></li>`).join('')}</ul></div>`;
document.getElementById('ranks').innerHTML =
  rankList('毛利率 Top10（收入≥10万）',[...big].sort((a,b)=>(b.gpm||0)-(a.gpm||0)).slice(0,10),r=>r.gpm,fmtP)+
  rankList('毛利率 Bottom10（收入≥10万）',[...big].sort((a,b)=>(a.gpm||0)-(b.gpm||0)).slice(0,10),r=>r.gpm,v=>`<span style="color:var(--red)">${fmtP(v)}</span>`)+
  rankList('单用户毛利 Top10',[...big].sort((a,b)=>(b.gp_user||0)-(a.gp_user||0)).slice(0,10),r=>r.gp_user,v=>'¥'+fmtD(v));

window.addEventListener('resize',()=>[sc,cb,tr].forEach(c=>c.resize()));
</script>
</body>
</html>"""

HTML = HTML.replace("__DATA__", DATA_JSON)
with open("南部大区城市看板/南部大区城市经营看板.html", "w", encoding="utf-8") as f:
    f.write(HTML)
print("HTML 已生成:", len(HTML)//1024, "KB")
