#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生命科学 2026Q3 汇报 PPTX → 自包含翻页式 HTML deck
铁律：① 严格照搬原稿文字与数字（一个字不改）② 零外部依赖 ③ 品牌套件注入
"""
import base64, html, json, os, re, sys

MODEL = sys.argv[1] if len(sys.argv) > 1 else '脚本/_tmp/deck_model.json'
OUT = sys.argv[2] if len(sys.argv) > 2 else '销售分析/生命科学销售部_2026三季度工作汇报_HTML版.html'
XLSJ = sys.argv[3] if len(sys.argv) > 3 else '脚本/_tmp/xls_p6.json'   # 第6页 Excel 附件数据

M = json.load(open(MODEL, encoding='utf-8'))
SW, SH = M['slideW'], M['slideH']
W, H = 1280, 720
K = W / SW

def PX(e):   return round((e or 0) * K, 2)
def FS(pt):  return round(pt * 96.0 / 72.0, 2) if pt else None
def esc(s):  return html.escape(s or '', quote=True)

MEDIA = {}
MEDIA_ORDER = []
def media(b64, ext, key=None):
    """图片只登记一次，返回 CSS 类名（data URI 写进 <style>，不在 DOM 里重复）"""
    k = key or b64[80:140]
    if k not in MEDIA:
        MEDIA[k] = 'data:image/%s;base64,%s' % ('jpeg' if ext == 'jpg' else ext, b64)
        MEDIA_ORDER.append(k)
    return 'm%d' % MEDIA_ORDER.index(k)

def flatten(shapes, par=None):
    """递归展开组合，输出每个形状在整页坐标系下的 EMU 位置 [x,y,w,h]"""
    out = []
    for sh in shapes:
        em = sh.get('em')
        if not em or em[0] is None:
            continue
        x, y, w, h = em[0], em[1] or 0, em[2] or 0, em[3] or 0
        if par:
            ax = par[0] + (x - par[4]) * par[2]
            ay = par[1] + (y - par[5]) * par[3]
            aw, ah = w * par[2], h * par[3]
        else:
            ax, ay, aw, ah = x, y, w, h
        s = dict(sh); s['A'] = [ax, ay, aw, ah]
        out.append(s)
        if sh['kind'] == 'group':
            go, gc = (sh.get('gxfrm') or [None, None])
            cp = par
            if go and gc and gc[2] and gc[3]:
                cp = (ax, ay, aw / gc[2], ah / gc[3], gc[0], gc[1])
            out.extend(flatten(sh.get('children') or [], cp))
    return out

import math

THEME = M.get('theme') or {}
SYS = {'#window': '#FFFFFF', '#windowText': '#000000'}
FF = {
    '微软雅黑': "'Microsoft YaHei','PingFang SC','Hiragino Sans GB',sans-serif",
    '思源黑体 CN Bold': "'Source Han Sans CN','Noto Sans CJK SC','PingFang SC',sans-serif",
    '思源黑体 CN': "'Source Han Sans CN','Noto Sans CJK SC','PingFang SC',sans-serif",
    '宋体': "SimSun,serif", '黑体': "SimHei,'PingFang SC',sans-serif",
    '等线': "DengXian,'PingFang SC',sans-serif",
}
PAL = ['#4874CB', '#EE822F', '#F2BA02', '#75BD42', '#30C0B4',
       '#E54C5E', '#7E57C2', '#26A69A', '#EF5350', '#8D6E63']

def res_col(c):
    if not c: return None
    c = SYS.get(c, c)
    if c.upper() == 'WINDOW': return '#FFFFFF'
    if c.upper() == 'WINDOWTEXT': return '#000000'
    return c if c.startswith('#') else '#' + c

def ffamily(f):
    if not f or f.startswith('+'): return "'PingFang SC','Microsoft YaHei',sans-serif"
    return FF.get(f) or ("'%s','PingFang SC',sans-serif" % f.replace("'", ''))

def rc(run, para=None, txt=None):
    """按 运行→段落→形状lstStyle→段落defRPr 顺序解析最终字色"""
    cands = []
    if run: cands.append((run.get('c'), run.get('sc')))
    if para: cands.append((para.get('c'), para.get('sc')))
    if txt:
        L = txt.get('lst') or {}
        cands.append((L.get('c'), L.get('sc')))
        for p in (txt.get('paras') or []):
            d = p.get('d') or {}
            cands.append((d.get('c'), d.get('sc')))
    for c, sc in cands:
        if c: return res_col(c)
        if sc and THEME.get(sc): return res_col(THEME[sc])
    return '#000000'

def sty(run, para=None, txt=None):
    run = run or {}; para = para or {}
    sz = run.get('sz') or para.get('sz')
    b = run.get('b')
    f = run.get('f') or para.get('f')
    if txt:
        L = txt.get('lst') or {}
        if not sz:
            sz = L.get('sz')
            if sz is None:
                for p in (txt.get('paras') or []):
                    d = p.get('d') or {}
                    if d.get('sz'): sz = d['sz']; break
        if b is None: b = L.get('b')
        if not f: f = L.get('f')
    return {'sz': sz or 18.0, 'b': bool(b), 'f': f, 'c': rc(run, para, txt)}

def span_style(s):
    st = ['font-size:%spx' % FS(s['sz']), 'color:%s' % s['c']]
    if s['b']: st.append('font-weight:700')
    if s['f']: st.append('font-family:%s' % ffamily(s['f']))
    return ';'.join(st)

def render_text(sh):
    A = sh['A']; t = sh['txt']
    x, y, w, h = PX(A[0]), PX(A[1]), PX(A[2]), PX(A[3])
    anc = (t.get('anchor') or 'TOP').upper()
    jc = {'MIDDLE': 'center', 'CTR': 'center', 'BOTTOM': 'flex-end'}.get(anc, 'flex-start')
    ps = []
    for p in t['paras']:
        rs = p.get('runs') or []
        al = (p.get('al') or '').upper()
        ta = {'CENTER': 'center', 'CTR': 'center', 'RIGHT': 'right',
              'JUSTIFY': 'justify'}.get(al, 'left')
        if not ''.join(r['t'] for r in rs).strip():
            ps.append('<p style="text-align:%s;font-size:24px;line-height:1.3">&nbsp;</p>' % ta)
            continue
        base = sty(rs[0], p, t)
        inner = ''.join('<span style="%s">%s</span>'
                        % (span_style(sty(r, p, t)), esc(r['t']).replace('\n', '<br>'))
                        for r in rs)
        ps.append('<p style="text-align:%s;font-size:%spx;line-height:1.28;margin:0">%s</p>'
                  % (ta, FS(base['sz']), inner))
    return ('<div class="tx" style="left:%spx;top:%spx;width:%spx;height:%spx;'
            'display:flex;flex-direction:column;justify-content:%s">%s</div>'
            % (x, y, w, h, jc, ''.join(ps)))

def render_table(sh, tags=None):
    tags = tags or {}
    A = sh['A']; x, y, w, h = PX(A[0]), PX(A[1]), PX(A[2]), PX(A[3])
    # ★夹紧到画幅内：原稿表格框常比版面宽（PowerPoint 直接裁掉），HTML 里会切掉最右一列的文字
    if x < 0: x = 0.0
    if y < 0: y = 0.0
    if x + w > W: w = W - x
    if y + h > H: h = H - y
    cw = sh.get('col_w') or []
    tot = sum([c for c in cw if c]) or 1
    colg = ''.join('<col style="width:%.4f%%">' % ((c or 0) / tot * 100) for c in cw)
    rows = []
    for ri, row in enumerate(sh['grid']):
        tds = []
        for ci, c in enumerate(row):
            if c.get('spanned'): continue
            at = ''
            if (c.get('span_h') or 1) > 1: at += ' rowspan="%d"' % c['span_h']
            if (c.get('span_w') or 1) > 1: at += ' colspan="%d"' % c['span_w']
            runs = c.get('runs') or []
            if runs:
                body = ''.join('<span style="%s">%s</span>'
                               % (span_style({'sz': r.get('sz') or 11.0,
                                              'b': r.get('b'), 'f': r.get('f'),
                                              'c': res_col(r.get('c')) or '#000000'}),
                                  esc(r['t']).replace('\n', '<br>'))
                               for r in runs)
            else:
                body = esc(c.get('t') or '').replace('\n', '<br>')
            ta = {'CENTER': 'center', 'RIGHT': 'right', 'LEFT': 'left'}.get(
                (c.get('al') or '').upper(), 'left')
            va = {'MIDDLE': 'middle', 'TOP': 'top', 'BOTTOM': 'bottom'}.get(
                (c.get('anchor') or 'MIDDLE').upper(), 'middle')
            st = 'text-align:%s;vertical-align:%s' % (ta, va)
            if c.get('fill'): st += ';background:#%s' % c['fill']
            tds.append('<td%s%s style="%s">%s</td>' % (at, tags.get((ri, ci), ''), st, body))
        rows.append('<tr>%s</tr>' % ''.join(tds))
    return ('<div class="tb" style="left:%spx;top:%spx;width:%spx;height:%spx">'
            '<table><colgroup>%s</colgroup>%s</table></div>'
            % (x, y, w, h, colg, ''.join(rows)))

def _svg(w, h, body):
    return ('<svg viewBox="0 0 %.0f %.0f" width="100%%" height="100%%" '
            'preserveAspectRatio="xMidYMid meet" '
            'font-family="PingFang SC,Microsoft YaHei,sans-serif">%s</svg>' % (w, h, body))

def col_svg(w, h, cats, sers):
    pl, pr, pt, pb = 54, 14, 14, 42
    iw, ih = w - pl - pr, h - pt - pb
    mx = max([float(v or 0) for s in sers for v in (s.get('vals') or [])] + [1.0])
    n = max(len(cats), 1)
    bw = iw / n
    g = []
    for k in range(5):
        yy = pt + ih * k / 4.0
        g.append('<line x1="%s" y1="%.1f" x2="%.1f" y2="%.1f" stroke="#e6ebf5"/>' % (pl, yy, w - pr, yy))
        g.append('<text x="%s" y="%.1f" font-size="9" fill="#8a94a6" text-anchor="end">%g</text>'
                 % (pl - 5, yy + 3, round(mx * (4 - k) / 4.0)))
    ns = max(len(sers), 1)
    barw = bw * 0.62 / ns
    for i, s in enumerate(sers):
        for j, v in enumerate(s.get('vals') or []):
            v = float(v or 0)
            bh = ih * v / mx
            g.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="%s" rx="1.5"/>'
                     % (pl + bw * j + bw * 0.19 + barw * i, pt + ih - bh, barw, bh, PAL[i % len(PAL)]))
    for j, c in enumerate(cats):
        g.append('<text x="%.1f" y="%.1f" font-size="8.5" fill="#5a6472" text-anchor="middle">%s</text>'
                 % (pl + bw * j + bw / 2, h - pb + 14, esc(str(c))))
    for i, s in enumerate(sers):
        g.append('<rect x="%d" y="%.1f" width="9" height="9" fill="%s" rx="1"/>'
                 % (pl + i * 86, h - 15, PAL[i % len(PAL)]))
        g.append('<text x="%d" y="%.1f" font-size="9.5" fill="#41474f">%s</text>'
                 % (pl + i * 86 + 13, h - 7, esc(str(s.get('name') or ''))))
    return _svg(w, h, ''.join(g))

def pie_svg(w, h, cats, sers):
    vals = (sers[0].get('vals') if sers else []) or []
    tot = sum(float(v or 0) for v in vals) or 1.0
    R = min(w * 0.50, h * 0.86) * 0.50
    cx, cy = w * 0.30, h / 2.0
    a0 = -90.0
    g = []
    for i, v in enumerate(vals):
        v = float(v or 0)
        if v <= 0: continue
        a1 = a0 + v / tot * 360.0
        x0 = cx + R * math.cos(math.radians(a0)); y0 = cy + R * math.sin(math.radians(a0))
        x1 = cx + R * math.cos(math.radians(a1)); y1 = cy + R * math.sin(math.radians(a1))
        g.append('<path d="M %.1f %.1f L %.1f %.1f A %.1f %.1f 0 %d 1 %.1f %.1f Z" fill="%s" '
                 'stroke="#fff" stroke-width="1.5"/>'
                 % (cx, cy, x0, y0, R, R, 1 if (a1 - a0) > 180 else 0, x1, y1, PAL[i % len(PAL)]))
        am = math.radians((a0 + a1) / 2)
        if v / tot > 0.06:
            g.append('<text x="%.1f" y="%.1f" font-size="10.5" fill="#fff" text-anchor="middle">%d%%</text>'
                     % (cx + R * 0.64 * math.cos(am), cy + R * 0.64 * math.sin(am) + 3.5, round(v * 100)))
        a0 = a1
    keep = [i for i, v in enumerate(vals) if float(v or 0) > 0]
    ly = cy - len(keep) * 8.0
    for i in keep:
        g.append('<rect x="%.1f" y="%.1f" width="9" height="9" fill="%s" rx="1"/>'
                 % (w * 0.60, ly - 8, PAL[i % len(PAL)]))
        g.append('<text x="%.1f" y="%.1f" font-size="10" fill="#41474f">%s  %d%%</text>'
                 % (w * 0.60 + 13, ly, esc(str(cats[i] if i < len(cats) else i + 1)),
                    round(float(vals[i]) * 100)))
        ly += 16
    return _svg(w, h, ''.join(g))

def render_chart(sh):
    """图表改为前端从同页表格单元格实时绘制 → 表格数字一改，图立刻跟着变。
    同时保留服务端预渲染 SVG 作兜底（JS 不可用时仍能看到原图）。"""
    A = sh['A']; x, y, w, h = PX(A[0]), PX(A[1]), PX(A[2]), PX(A[3])
    c = sh.get('chart') or {}
    ct = (c.get('type') or '').upper()
    cats = list(c.get('cats') or [])
    sers = c.get('series') or []
    if sers and not cats:
        cats = [str(i + 1) for i in range(len(sers[0].get('vals') or []))]
    kind = 'pie' if 'PIE' in ct else 'col'
    init = pie_svg(w, h, cats, sers) if kind == 'pie' else col_svg(w, h, cats, sers)
    names = '|'.join([str(s.get('name') or '') for s in sers])
    return ('<div class="ch" data-kind="%s" data-names="%s" '
            'style="left:%spx;top:%spx;width:%spx;height:%spx">%s</div>'
            % (kind, esc(names), x, y, w, h, init))

def chart_tags(n):
    """{(行,列): 附加属性} —— 让表格单元格成为图表的唯一数据源。
    表结构（全年12月版）：
      P3：r0 标题 / r1 表头(1月..12月 在 col 4..15) / r2=2026年 / r3=2025年
      P7：r0 表头 / r1..r10 十个品类 / r11 合计行；合计列在 col 13
    """
    t = {}
    if n == 3:
        for j in range(12):
            t[(1, 4 + j)] = ' data-cat="%d"' % j
            t[(2, 4 + j)] = ' data-s="0" data-i="%d"' % j
            t[(3, 4 + j)] = ' data-s="1" data-i="%d"' % j
    elif n == 7:
        for i in range(10):
            t[(1 + i, 0)] = ' data-cat="%d"' % i
            # 合计列：既是饼图数据源，又是「行合计」自动重算锚点
            t[(1 + i, 13)] = ' data-v="%d" data-rsum="%d"' % (i, i + 1)
        for ci in range(1, 13):
            t[(11, ci)] = ' data-csum="%d"' % ci
        t[(11, 13)] = ' data-gsum="1"'
    return t

def xls_panel():
    """第6页：Excel 附件三张表 → 页签式数据面板"""
    if not os.path.exists(XLSJ):
        return ''
    sheets = json.load(open(XLSJ, encoding='utf-8'))
    tabs, pans = [], []
    for i, s in enumerate(sheets):
        tabs.append('<button class="xt%s" data-t="%d">%s</button>'
                    % (' on' if i == 0 else '', i, esc(s['name'])))
        trs = []
        for ri, r in enumerate(s['rows']):
            tag = 'th' if ri == 0 else 'td'
            trs.append('<tr>%s</tr>' % ''.join(
                '<%s>%s</%s>' % (tag, esc(c), tag) for c in r['cells']))
        pans.append('<div class="xp%s"><table class="xtb">%s</table></div>'
                    % (' on' if i == 0 else '', ''.join(trs)))
    return ('<div class="xls" style="left:64px;top:232px;width:1152px;height:412px">'
            '<div class="xtabs">%s</div><div class="xbody">%s</div></div>'
            % (''.join(tabs), ''.join(pans)))

def slide_html(sl):
    own = flatten(sl['shapes'])
    keys = set((s.get('sha'), round(PX(s['A'][0])), round(PX(s['A'][1])))
               for s in own if s['kind'] == 'pic')
    layers = [s for s in flatten(sl.get('layshapes') or [])
              if not (s['kind'] == 'pic'
                      and (s.get('sha'), round(PX(s['A'][0])), round(PX(s['A'][1]))) in keys)]
    layers += own
    tags = chart_tags(sl['n'])
    bg, body = None, []
    for sh in layers:
        A = sh['A']; k = sh['kind']
        if k == 'pic':
            if A[2] >= SW * 0.98 and A[3] >= SH * 0.98 and bg is None:
                bg = media(sh['b64'], sh['ext'], sh.get('sha')); continue
            body.append('<div class="im %s" style="left:%spx;top:%spx;width:%spx;height:%spx"></div>'
                        % (media(sh['b64'], sh['ext'], sh.get('sha')), PX(A[0]), PX(A[1]), PX(A[2]), PX(A[3])))
        elif k == 'text':
            body.append(render_text(sh))
        elif k == 'table':
            body.append(render_table(sh, tags))
        elif k == 'chart':
            body.append(render_chart(sh))
        elif k == 'shape':
            if A[2] <= SW * 0.005 and A[3] > A[2]:
                body.append('<div class="ln" style="left:%spx;top:%spx;width:1.5px;height:%spx"></div>'
                            % (PX(A[0]), PX(A[1]), PX(A[3])))
            elif A[3] <= SH * 0.005 and A[2] > A[3]:
                body.append('<div class="ln" style="left:%spx;top:%spx;width:%spx;height:1.5px"></div>'
                            % (PX(A[0]), PX(A[1]), PX(A[2])))
            elif sh.get('bg'):
                body.append('<div style="position:absolute;left:%spx;top:%spx;width:%spx;height:%spx;'
                            'background:#%s;border-radius:2px"></div>'
                            % (PX(A[0]), PX(A[1]), PX(A[2]), PX(A[3]), sh['bg']))
        elif k == 'ole':
            if sl['n'] == 6:
                _xp = xls_panel()      # ★第6页：用附件 Excel 的真实内容替换内嵌对象占位
                if _xp:
                    body.append(_xp)
                    continue
            body.append('<div class="ole" style="left:%spx;top:%spx;width:%spx;height:%spx">'
                        '原稿内嵌对象<br><span>（HTML 版无法呈现）</span></div>'
                        % (PX(A[0]), PX(A[1]), PX(A[2]), PX(A[3])))
    cls = ('slide-bg ' + bg) if bg else 'slide-bg'
    st = '' if bg else ' style="background:#fff"'
    return ('<section class="slide"><div class="%s"%s></div>%s</section>'
            % (cls, st, ''.join(body)))

TITLES = {
    1: '生命科学销售部2026年三季度工作总结报告',
    2: '汇报内容',
    14: 'Thanks',
}

def slide_title(sl, n):
    if n in TITLES: return TITLES[n]
    for sh in sl['shapes']:
        t = ((sh.get('txt') or {}).get('plain') or '').strip()
        if re.match(r'^0[123][^\d]', t) and '附件' not in t:
            return t
    return '第 %d 页' % n

SEC = {'01': '目标达成', '02': '各类产品销售', '03': '市场与竞品动态'}

def toc_html():
    out = []
    for k, sl in enumerate(M['slides']):
        n = sl['n']
        t = slide_title(sl, n)
        m = re.match(r'^(0[123])(.*)$', t)
        if n == 1:      badge, lab = '', '封面'
        elif n == 2:    badge, lab = '', '汇报内容'
        elif n == 14:   badge, lab = '', '封底 · Thanks'
        else:           badge, lab = (m.group(1) if m else ''), ((m.group(2) or '').strip() if m else t)
        out.append('<a class="it%s" data-k="%d"><i class="b">%s</i><span>%s</span></a>'
                   % (' on' if k == 0 else '', k, badge, esc(lab)))
    return ''.join(out)

DECK = []
for k, sl in enumerate(M['slides']):
    h = slide_html(sl)
    if k == 0:
        h = h.replace('class="slide"', 'class="slide show"', 1)
    DECK.append(h)
DECK = ''.join(DECK)

MEDIA_CSS = '\n'.join('.%s{background-image:url(%s)}' % ('m%d' % i, MEDIA[k])
                      for i, k in enumerate(MEDIA_ORDER))

CSS = """*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{height:100vh;display:flex;flex-direction:column;background:#eef2f9;overflow:hidden;
 font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}
.app{flex:1;display:flex;min-height:0}
.side{width:238px;flex:none;background:#fff;border-right:1px solid #e3e8f0;overflow-y:auto;padding:6px 0 14px}
.side h2{font-size:11px;color:#8a94a6;margin:12px 16px 5px;font-weight:600;letter-spacing:1px}
.it{display:flex;gap:8px;align-items:baseline;padding:8px 14px;cursor:pointer;font-size:13px;
 color:#374253;border-left:3px solid transparent;text-decoration:none}
.it:hover{background:#f4f7fd}
.it.on{background:#eaf1fd;border-left-color:#4874CB;color:#1852B6;font-weight:600}
.it .b{font-size:10px;color:#9aa4b5;min-width:18px;font-style:normal}
.it.on .b{color:#4874CB}
.main{flex:1;position:relative;min-width:0;display:flex;flex-direction:column}
.stage{flex:1;display:flex;align-items:center;justify-content:center;min-height:0;padding:14px;overflow:hidden}
.deckwrap{position:relative}
.deck{position:absolute;left:0;top:0;width:1280px;height:720px;transform-origin:top left;
 background:#fff;box-shadow:0 8px 30px rgba(16,42,90,.18);border-radius:4px;overflow:hidden}
.slide{position:absolute;inset:0;display:none}
.slide.show{display:block}
.slide-bg{position:absolute;inset:0;background-size:100% 100%;background-repeat:no-repeat}
.tx{position:absolute;white-space:pre-wrap;word-break:break-word}
.im{position:absolute;background-size:100% 100%;background-repeat:no-repeat}
.tb{position:absolute}
.tb table{border-collapse:collapse;width:100%;height:100%;table-layout:fixed}
.tb td{border:1px solid #cfd8e6;padding:2px 5px;font-size:11px;overflow:hidden;color:#1f2a3d}
.ch{position:absolute}
.ln{position:absolute;background:#fff;opacity:.9}
.ole{position:absolute;display:flex;flex-direction:column;align-items:center;justify-content:center;
 border:1.5px dashed #b9c4d6;border-radius:6px;color:#8a94a6;font-size:11px;text-align:center;line-height:1.6}
.xls{position:absolute;background:#fff;border:1px solid #dbe3ef;border-radius:8px;display:flex;
 flex-direction:column;overflow:hidden;box-shadow:0 2px 10px rgba(16,42,90,.08)}
.xtabs{flex:none;display:flex;gap:6px;padding:7px 10px;background:#f4f7fd;border-bottom:1px solid #e3e8f0}
.xt{border:1px solid #cfd8e6;background:#fff;border-radius:6px;padding:4px 12px;cursor:pointer;
 font-size:12px;color:#4a5568;font-family:inherit}
.xt.on{background:#4874CB;border-color:#4874CB;color:#fff;font-weight:600}
.xbody{flex:1;overflow:auto}
.xp{display:none}
.xp.on{display:block}
.xtb{border-collapse:collapse;font-size:10.5px;white-space:nowrap;width:100%}
.xtb th,.xtb td{border:1px solid #dbe3ef;padding:3px 7px;color:#243044;text-align:left;vertical-align:top}
.xtb th{background:#eaf1fd;color:#1852B6;font-weight:600;position:sticky;top:0;z-index:1}
.xtb td:first-child{font-weight:600;color:#1852B6}
.bar{flex:none;display:flex;align-items:center;gap:10px;padding:8px 16px;background:#fff;
 border-top:1px solid #e3e8f0;font-size:13px;color:#374253}
.btn{border:1px solid #cfd8e6;background:#fff;border-radius:6px;padding:5px 13px;cursor:pointer;
 font-size:13px;color:#374253;font-family:inherit}
.btn:hover{background:#f4f7fd;border-color:#4874CB;color:#1852B6}
.btn:disabled{opacity:.35;cursor:default}
.btn.on{background:#eaf1fd;border-color:#4874CB;color:#1852B6}
.hero{font-weight:600;color:#1852B6;margin-left:auto;display:flex;align-items:center;gap:10px}
body.pres .side,body.pres .bar,body.pres .bior-header,body.pres .bior-footer{display:none!important}
body.pres .stage{padding:0}
body.edit .tx,body.edit .tb td{outline:1px dashed #9fc0ee;outline-offset:1px}
@media(max-width:860px){.side{display:none}
 .stage{padding:4px}
 .bar{flex-wrap:wrap;gap:5px;padding:5px 8px;font-size:11px}
 .bar .btn{padding:3px 8px;font-size:11px;border-radius:5px}
 .bar .hero{display:none}}
@media print{body{height:auto;overflow:visible;display:block}.side,.bar{display:none!important}
 .deck{position:static;width:100%;height:auto;box-shadow:none}
 .slide{position:relative;display:block!important;height:720px;page-break-after:always}}"""

JS = """var N=__N__,i=0;
function show(k){i=Math.max(0,Math.min(N-1,k));
 var sl=document.querySelectorAll('.slide'),it=document.querySelectorAll('.it');
 for(var j=0;j<sl.length;j++)sl[j].classList.toggle('show',j===i);
 for(var j=0;j<it.length;j++){it[j].classList.toggle('on',j===i);
  if(j===i&&it[j].scrollIntoView)it[j].scrollIntoView({block:'nearest'});}
 document.getElementById('pg').textContent=(i+1)+' / '+N;
 document.getElementById('prev').disabled=(i===0);document.getElementById('next').disabled=(i===N-1);
 try{history.replaceState(null,'','#p'+(i+1));}catch(e){}}
function fit(){var st=document.querySelector('.stage'),d=document.querySelector('.deck'),
 w=d.parentNode;var k=Math.min(st.clientWidth/1280,st.clientHeight/720);
 d.style.transform='scale('+k+')';w.style.width=(1280*k)+'px';w.style.height=(720*k)+'px';}
function go(k){show(k);}
function fs(){if(!document.fullscreenElement){document.documentElement.requestFullscreen();
 document.body.classList.add('pres');}else{document.exitFullscreen();}}
document.addEventListener('fullscreenchange',function(){
 if(!document.fullscreenElement)document.body.classList.remove('pres');});
function edit(){var on=document.body.classList.toggle('edit');
 var els=document.querySelectorAll('.tx,.tb td');
 for(var j=0;j<els.length;j++)els[j].contentEditable=on?'true':'false';
 document.getElementById('ed').classList.toggle('on',on);
 document.getElementById('ed').textContent=on?'✅ 编辑中（点此退出）':'✏️ 编辑文字';}
document.addEventListener('keydown',function(e){
 var t=e.target||{};if(t.isContentEditable)return;
 if(e.key==='ArrowRight'||e.key==='ArrowDown'||e.key===' '||e.key==='PageDown'){e.preventDefault();show(i+1);}
 else if(e.key==='ArrowLeft'||e.key==='ArrowUp'||e.key==='PageUp'){e.preventDefault();show(i-1);}
 else if(e.key==='Home'){e.preventDefault();show(0);}
 else if(e.key==='End'){e.preventDefault();show(N-1);}
 else if(e.key==='f'||e.key==='F'){fs();}
 else if(e.key==='e'||e.key==='E'){edit();}});
document.querySelectorAll('.it').forEach(function(a){
 a.addEventListener('click',function(){show(+a.getAttribute('data-k'));});});
var x0=null;
document.addEventListener('touchstart',function(e){x0=e.touches[0].clientX;},{passive:true});
document.addEventListener('touchend',function(e){if(x0===null)return;var dx=e.changedTouches[0].clientX-x0;
 if(Math.abs(dx)>60){show(dx<0?i+1:i-1);}x0=null;},{passive:true});
var PAL=['#4874CB','#EE822F','#F2BA02','#75BD42','#30C0B4','#E54C5E','#7E57C2','#26A69A','#EF5350','#8D6E63'];
function _n(t){t=String(t==null?'':t).replace(/[^0-9.]/g,'');return t===''?0:parseFloat(t);}
function _e(s){return String(s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});}
function _svg(w,h,b){return '<svg viewBox="0 0 '+w+' '+h+'" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" font-family="PingFang SC,Microsoft YaHei,sans-serif">'+b+'</svg>';}
function dCol(w,h,cats,sers){
 var pl=54,pr=14,pt=14,pb=42,iw=w-pl-pr,ih=h-pt-pb,mx=1,i,j,k;
 for(i=0;i<sers.length;i++)for(j=0;j<sers[i].vals.length;j++){var v0=sers[i].vals[j]||0;if(v0>mx)mx=v0;}
 var n=Math.max(cats.length,1),bw=iw/n,ns=Math.max(sers.length,1),gw=bw*0.62/ns,g=[];
 for(k=0;k<5;k++){var yy=pt+ih*k/4;
  g.push('<line x1="'+pl+'" y1="'+yy.toFixed(1)+'" x2="'+(w-pr)+'" y2="'+yy.toFixed(1)+'" stroke="#e6ebf5"/>');
  g.push('<text x="'+(pl-5)+'" y="'+(yy+3).toFixed(1)+'" font-size="9" fill="#8a94a6" text-anchor="end">'+Math.round(mx*(4-k)/4)+'</text>');}
 for(i=0;i<sers.length;i++){var sv=sers[i].vals||[];for(j=0;j<sv.length;j++){
  var v=sv[j]||0,bh=ih*v/mx;
  g.push('<rect x="'+(pl+bw*j+bw*0.19+gw*i).toFixed(1)+'" y="'+(pt+ih-bh).toFixed(1)+'" width="'+gw.toFixed(1)+'" height="'+bh.toFixed(1)+'" fill="'+PAL[i%10]+'" rx="1.5"/>');}}
 for(j=0;j<cats.length;j++)
  g.push('<text x="'+(pl+bw*j+bw/2).toFixed(1)+'" y="'+(h-pb+14).toFixed(1)+'" font-size="8.5" fill="#5a6472" text-anchor="middle">'+_e(cats[j]||'')+'</text>');
 for(i=0;i<sers.length;i++){
  g.push('<rect x="'+(pl+i*86)+'" y="'+(h-15).toFixed(1)+'" width="9" height="9" fill="'+PAL[i%10]+'" rx="1"/>');
  g.push('<text x="'+(pl+i*86+13)+'" y="'+(h-7).toFixed(1)+'" font-size="9.5" fill="#41474f">'+_e(sers[i].name||'')+'</text>');}
 return _svg(w,h,g.join(''));
}
function dPie(w,h,cats,vals){
 var tot=0,i;for(i=0;i<vals.length;i++)tot+=(vals[i]||0)||0;if(!tot)tot=1;
 var R=Math.min(w*0.50,h*0.86)*0.50,cx=w*0.30,cy=h/2,a0=-90,g=[],keep=[];
 for(i=0;i<vals.length;i++){var v=vals[i]||0;if(v<=0)continue;var a1=a0+v/tot*360;
  var x0=cx+R*Math.cos(a0*Math.PI/180),y0=cy+R*Math.sin(a0*Math.PI/180),
      x1=cx+R*Math.cos(a1*Math.PI/180),y1=cy+R*Math.sin(a1*Math.PI/180);
  g.push('<path d="M '+cx.toFixed(1)+' '+cy.toFixed(1)+' L '+x0.toFixed(1)+' '+y0.toFixed(1)+' A '+R.toFixed(1)+' '+R.toFixed(1)+' 0 '+(((a1-a0)>180)?1:0)+' 1 '+x1.toFixed(1)+' '+y1.toFixed(1)+' Z" fill="'+PAL[i%10]+'" stroke="#fff" stroke-width="1.5"/>');
  if(v/tot>0.06){var am=(a0+a1)/2*Math.PI/180;
   g.push('<text x="'+(cx+R*0.64*Math.cos(am)).toFixed(1)+'" y="'+(cy+R*0.64*Math.sin(am)+3.5).toFixed(1)+'" font-size="10.5" fill="#fff" text-anchor="middle">'+Math.round(v/tot*100)+'%</text>');}
  a0=a1;keep.push(i);}
 var ly=cy-keep.length*8;
 for(i=0;i<keep.length;i++){var kk=keep[i];
  g.push('<rect x="'+(w*0.60).toFixed(1)+'" y="'+(ly-8).toFixed(1)+'" width="9" height="9" fill="'+PAL[kk%10]+'" rx="1"/>');
  g.push('<text x="'+(w*0.60+13).toFixed(1)+'" y="'+ly.toFixed(1)+'" font-size="10" fill="#41474f">'+_e(cats[kk]||(''+(kk+1)))+'  '+Math.round((vals[kk]||0)/tot*100)+'%</text>');
  ly+=16;}
 return _svg(w,h,g.join(''));
}
function _fmt(v){return (Math.abs(v-Math.round(v))<1e-6)?(''+Math.round(v)):(''+(Math.round(v*10000)/10000));}
function resum(){
 var anc=document.querySelectorAll('[data-rsum]');
 if(!anc.length)return;
 var tb=anc[0].closest('table');if(!tb)return;
 var trs=tb.querySelectorAll('tr'),i,j,s,td,tr,tds,has,t;
 for(i=0;i<anc.length;i++){
  td=anc[i];tr=trs[+td.getAttribute('data-rsum')];if(!tr)continue;
  tds=tr.querySelectorAll('td');s=0;has=false;
  for(j=1;j<=12;j++){t=tds[j]?tds[j].textContent:'';if(t.trim()!==''){has=true;s+=_n(t);}}
  td.textContent=has?_fmt(s):'';
 }
 var cs=document.querySelectorAll('[data-csum]');
 for(i=0;i<cs.length;i++){
  td=cs[i];var ci=+td.getAttribute('data-csum');s=0;has=false;
  for(j=1;j<=10;j++){tr=trs[j];if(!tr)continue;tds=tr.querySelectorAll('td');
   if(tds[ci]){t=tds[ci].textContent;if(t.trim()!==''){has=true;s+=_n(t);}}}
  td.textContent=has?_fmt(s):'';
 }
 var g=document.querySelector('[data-gsum]');
 if(g){s=0;has=false;
  for(i=0;i<anc.length;i++){t=anc[i].textContent;if(t.trim()!==''){has=true;s+=_n(t);}}
  g.textContent=has?_fmt(s):'';}
}
function drawCharts(){
 var bx=document.querySelectorAll('.ch[data-kind]');
 for(var b=0;b<bx.length;b++){
  var box=bx[b];
  if(!box.closest||!box.closest('.tb')&&!box.parentNode)continue;
  var sl=box.parentNode;while(sl&&sl.className&&sl.className.indexOf('slide')<0)sl=sl.parentNode;
  if(!sl)continue;
  var kind=box.getAttribute('data-kind'),cats=[],m={},vals=[],i,el;
  var cs=sl.querySelectorAll('[data-cat]');
  for(i=0;i<cs.length;i++)cats[+cs[i].getAttribute('data-cat')]=cs[i].textContent.trim();
  var ss=sl.querySelectorAll('[data-s]');
  for(i=0;i<ss.length;i++){var s=+ss[i].getAttribute('data-s'),k=+ss[i].getAttribute('data-i');
   if(!m[s])m[s]=[];m[s][k]=_n(ss[i].textContent);}
  var vs=sl.querySelectorAll('[data-v]');
  for(i=0;i<vs.length;i++)vals[+vs[i].getAttribute('data-v')]=_n(vs[i].textContent);
  var W=parseFloat(box.style.width)||600,H=parseFloat(box.style.height)||300;
  if(kind==='pie'){box.innerHTML=dPie(W,H,cats,vals);}
  else{var names=(box.getAttribute('data-names')||'').split('|'),sers=[],ks=Object.keys(m).sort();
   for(i=0;i<ks.length;i++){var vv=m[ks[i]]||[],any=false,j;
    for(j=0;j<vv.length;j++)if(vv[j]){any=true;break;}
    if(any)sers.push({name:names[ks[i]]||('系列'+(+ks[i]+1)),vals:vv});}
   box.innerHTML=sers.length?dCol(W,H,cats,sers)
    :'<div style="display:flex;height:100%;align-items:center;justify-content:center;color:#a8b2c1;font-size:12px">暂无数据 — 在「编辑文字」模式下填入表格数字后自动生成</div>';}
 }
}
var _dt=null;
document.addEventListener('input',function(e){
 if(!e.target||!e.target.closest||!e.target.closest('.tb,.xtb'))return;
 clearTimeout(_dt);_dt=setTimeout(function(){resum();drawCharts();},120);});
function tabs(){
 var ps=document.querySelectorAll('.xls .xp'),i;
 for(i=0;i<ps.length;i++)ps[i].setAttribute('data-p',''+i);
 var bs=document.querySelectorAll('.xt');
 for(i=0;i<bs.length;i++)(function(btn){
  btn.addEventListener('click',function(){
   var box=btn.parentNode;while(box&&!(box.className||'').match(/xls/))box=box.parentNode;
   if(!box)return;var k=btn.getAttribute('data-t');
   var x=box.querySelectorAll('.xt');for(var j=0;j<x.length;j++)x[j].classList.toggle('on',x[j]===btn);
   var p=box.querySelectorAll('.xp');for(var j=0;j<p.length;j++)p[j].classList.toggle('on',p[j].getAttribute('data-p')===k);
  });})(bs[i]);
}
tabs();drawCharts();   // 注：初始不调用 resum()，保持原稿 0.699/2.3766 原样；编辑后由 input 事件触发重算
window.addEventListener('resize',fit);
fit();var m=(location.hash||'').match(/^#p(\\d+)$/);show(m?+m[1]-1:0);fit();
""".replace('__N__', str(len(M['slides'])))

HTML = """<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>生命科学销售部2026年三季度工作总结报告</title>
<style>%s</style></head><body>
<div class="app">
 <nav class="side"><h2>汇报目录</h2>%s</nav>
 <div class="main">
  <div class="stage"><div class="deckwrap"><div class="deck">%s</div></div></div>
  <div class="bar">
   <button class="btn" id="prev" onclick="show(i-1)">‹ 上一页</button>
   <button class="btn" id="next" onclick="show(i+1)">下一页 ›</button>
   <span id="pg" style="min-width:64px;text-align:center;color:#1852B6;font-weight:600"></span>
   <button class="btn" onclick="fs()">⛶ 全屏演示</button>
   <button class="btn" id="ed" onclick="edit()">✏️ 编辑文字</button>
   <button class="btn" onclick="resum();drawCharts()">🔄 刷新图表</button>
   <button class="btn" onclick="window.print()">🖨 导出PDF</button>
   <span class="hero"><span>← → 翻页 ｜ F 全屏 ｜ E 编辑</span></span>
  </div>
 </div>
</div>
<script>%s</script></body></html>""" % (CSS + '\n' + MEDIA_CSS, toc_html(), DECK, JS)

try:
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    import brand_kit as BK
    HTML = BK.inject(HTML, theme='dark', tag='营销中心 · 生命科学销售部')
    _v = BK.verify(HTML)
    print('  品牌自检: %s' % (' '.join('%s=%s' % (k, v) for k, v in _v.items())))
    assert all(_v.values()), '品牌自检未通过: %s' % [k for k, v in _v.items() if not v]
except AssertionError:
    raise
except Exception as e:
    print('  ⚠️ 品牌注入跳过: %s' % e)

open(OUT, 'w', encoding='utf-8').write(HTML)
print('✅ 已生成 %s' % OUT)
print('   大小 %.2f MB | %d 张 | 内嵌图 %d 张' % (len(HTML.encode()) / 1048576.0, len(M['slides']), len(MEDIA)))

