From d357206130745fa7168430318967967d674ba5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A7=E9=BE=99?= Date: Tue, 9 Jun 2026 18:14:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20GLM-4-Flash=20=E7=BE=8E=E5=8C=96?= =?UTF-8?q?=E7=89=88=E7=9C=8B=E6=9D=BF(=E6=B5=85=E7=B1=B3=E9=BB=84?= =?UTF-8?q?=E8=93=9D=E9=87=91=E5=95=86=E5=8A=A1=E9=A3=8E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 3 个 _glm.py 脚本(HTML×2 + PPTX×1) - GLM 实时设计 CSS 主题(浅米黄 #FAF6EE + 蓝金 #1A3A5C/#C8962E) - 强制约定类名 + 后处理纠错(防4px gold语法错) - 60s timeout + retry, 失败 fallback 到 CHEC 品牌色手写 CSS - PPTX 国别改用横向进度条(原版饼图), 字号 44→52 - 产出 *_glm.html × 2 + _GLM版.pptx, 与原版并存 数据源: 复用 06-08 三个清洗产物, 不重算, 防 GLM 幻觉 --- .../certified_schemes_dashboard_glm.html | 156 ++++++++ data/2026-06-08/cleaned/dashboard_glm.html | 240 +++++++++++++ .../cleaned/危大方案编审进度看板_GLM版.pptx | Bin 0 -> 39988 bytes src/b4_dashboard_html_glm.py | 295 +++++++++++++++ src/b4b_certified_dashboard_glm.py | 280 +++++++++++++++ src/gen_pptx_glm.py | 335 ++++++++++++++++++ 6 files changed, 1306 insertions(+) create mode 100644 data/2026-06-08/cleaned/certified_schemes_dashboard_glm.html create mode 100644 data/2026-06-08/cleaned/dashboard_glm.html create mode 100644 data/2026-06-08/cleaned/危大方案编审进度看板_GLM版.pptx create mode 100644 src/b4_dashboard_html_glm.py create mode 100644 src/b4b_certified_dashboard_glm.py create mode 100644 src/gen_pptx_glm.py diff --git a/data/2026-06-08/cleaned/certified_schemes_dashboard_glm.html b/data/2026-06-08/cleaned/certified_schemes_dashboard_glm.html new file mode 100644 index 0000000..544e239 --- /dev/null +++ b/data/2026-06-08/cleaned/certified_schemes_dashboard_glm.html @@ -0,0 +1,156 @@ + + + + + +危大方案编审进度看板 · GLM 美化版 | 中东区域公司 + + + + +
+

🏗️ 危大方案编审进度看板

+

2026 年开工项目 · 中东区域公司 · 数据日期 2026-06-08 · GLM 美化版

+
+ + +
+
方案总数
52
+
已审批
29
+
审批率
56%
+
超规方案
22
+
未完成
23
+
橙色预警
1
+
红色预警
0
+
覆盖项目
12
+
+ + +
+

⚠️ 预警方案明细

+ + + +
项目名称方案名称国别计划开工预警等级
阿联酋迪拜马克图姆国际机场地下结构工程项目BHS处理中心/GSE隧道现浇板专项施工方案(4包)...阿拉伯联合酋长国2026-06-10 00:00:00orange
+
+ + +
+

📋 项目明细(按方案总数排序)

+ + + + + + + + + + + + + + + + + + +
项目名称方案已审批超规进度审批率国别
阿联酋迪拜马克图姆国际机场地下结构工程项目352013
57%阿拉伯联合酋长国
阿联酋沙迦卡尔巴摩托艇港开发项目301
0%阿拉伯联合酋长国
阿联酋阿布扎比汽车基地房建项目302
0%阿拉伯联合酋长国
沙特吉赞基础下游工业城5至7号路间矿业区基础设施一期项目220
100%沙特阿拉伯
阿联酋阿布扎比汽车基地基础设施项目202
0%阿拉伯联合酋长国
沙特利雅得德拉伊耶门二期多功能场馆及办公楼房建项目111
100%沙特阿拉伯
沙特利雅得南二环路第三标段项目110
100%沙特阿拉伯
卡塔尔道路运营及维护框架项目111
100%卡塔尔
沙特吉赞基础下游工业城3区1巷独栋别墅一期项目111
100%沙特阿拉伯
沙特达曼港第一和第二集装箱码头升级改造工程111
100%沙特阿拉伯
阿联酋阿布扎比哈里发港EGA泊位翻新工程项目110
100%阿拉伯联合酋长国
阿联酋阿布扎比哈里发工业园B区食品基地工程110
100%阿拉伯联合酋长国
合计522922
56%
+
+ + +
+

🌍 按国别汇总

+ + + + + +
国别项目数方案总数已审批超规审批率
卡塔尔1111100%
沙特阿拉伯5663100%
阿拉伯联合酋长国645221849%
+
+ +
+

中国港湾中东区域公司 技术部 · GLM-4-Flash 美化版 · 自动生成于 2026-06-08

+
+ + \ No newline at end of file diff --git a/data/2026-06-08/cleaned/dashboard_glm.html b/data/2026-06-08/cleaned/dashboard_glm.html new file mode 100644 index 0000000..da53cba --- /dev/null +++ b/data/2026-06-08/cleaned/dashboard_glm.html @@ -0,0 +1,240 @@ + + + + + +中东区域公司 技术部月报台账看板 · GLM 美化版 | 2026-06-08 + + + + +
+

🏗️ 中东区域公司 · 技术部月报台账看板

+

数据日期: 2026-06-08 | 生成时间: 2026-06-09 | GLM-4-Flash 美化版

+
+ +
+
+

📋 危大方案编审进度

+
有效方案总数135
+
超一定规模52
+
已审批112
+
审批率83%
+
预警信号5
+
覆盖项目38
+
+ +
+

🔄 项目启动动态跟踪

+
跟踪项目数80
+
总任务数5290
+
已完成1443
+
逾期803
+
完成率27%
+
+ +
+

💰 营业额与产值

+
中东项目数97
+
活跃在建47
+
签约合同额$95万
+
本年营业收入$5万
+
累计营业收入$26万
+
+
+ +
+

📋 危大方案编审 — 项目排行 Top 15

+ + + + + + + + + + + + + + + + + +
项目名称方案已审批超规审批进度审批率国别
阿联酋迪拜马克图姆国际机场地下结构工程项目352013
57%阿拉伯联合酋长国
沙特红海Laheq岛连接路和跨海桥工程11117
100%沙特阿拉伯
沙特利雅得南二环路第三标段项目10102
100%沙特阿拉伯
阿联酋阿布扎比中岛公园一期1C道桥项目773
100%阿拉伯联合酋长国
沙特吉达市中心综合开发基础设施项目773
100%沙特阿拉伯
沙特吉赞基础下游工业城保税区至人工岛连接桥项目663
100%沙特阿拉伯
阿联酋哈伊马角永利岛连接桥661
100%阿拉伯联合酋长国
阿联酋阿布扎比哈里发工业园B区食品基地工程440
100%阿拉伯联合酋长国
沙特达曼港第一和第二集装箱码头升级改造工程332
100%沙特阿拉伯
沙特红海舒莱亚码头及潜水中心项目330
100%沙特阿拉伯
沙特吉赞基础下游工业城3区1巷独栋别墅一期项目332
100%沙特阿拉伯
沙特利雅得德拉伊耶门二期多功能场馆及办公楼房建项目331
100%沙特阿拉伯
阿联酋沙迦卡尔巴摩托艇港开发项目301
0%阿拉伯联合酋长国
阿联酋阿布扎比汽车基地房建项目302
0%阿拉伯联合酋长国
阿联酋阿布扎比瑞姆岛7号和8号跨海桥工程222
100%阿拉伯联合酋长国
+
+ +
+

🔄 项目启动跟踪 — 逾期项目 Top 15

+ + + + + + + + + + + + + + + + + +
项目名称总任务已完成逾期完成率
阿联酋阿布扎比汽车基地房建项目641392%
沙特苏盖克电厂缆桩修复工程项目66213232%
卡塔尔道路运营及维护框架项目666309%
阿联酋高铁迪拜段土建项目66102915%
沙特利雅得南二环路第三标段项目655288%
沙特红海拉赫克岛疏浚吹填工程67222233%
阿联酋沙迦卡尔巴摩托艇港开发项目665228%
阿联酋阿布扎比西部三岛水工项目6682212%
沙特海尔港钢厂前期工作土方工程67202030%
沙特吉达市中心综合开发基础设施项目67162024%
阿联酋阿布扎比马斯努阿岛水工项目64162025%
阿联酋富查伊拉军港扩建附属设施包6592014%
阿联酋阿布扎比沙哈马港升级改造项目64131920%
沙特穆卡布四方城土方包67211831%
沙特红海Laheq岛连接路和跨海桥工程67211731%
+
+ +
+

💰 营收项目 Top 10(本年营收排序)

+ + + +
项目名称签约合同额(万美元)本年营收累计营收
暂无营收数据
+
+ + + + \ No newline at end of file diff --git a/data/2026-06-08/cleaned/危大方案编审进度看板_GLM版.pptx b/data/2026-06-08/cleaned/危大方案编审进度看板_GLM版.pptx new file mode 100644 index 0000000000000000000000000000000000000000..ec2e05de4e8cc8c14ef1ef803a1d854c08bf58f1 GIT binary patch literal 39988 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 G(`SE{C(kAT literal 0 HcmV?d00001 diff --git a/src/b4_dashboard_html_glm.py b/src/b4_dashboard_html_glm.py new file mode 100644 index 0000000..c466750 --- /dev/null +++ b/src/b4_dashboard_html_glm.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +""" +GLM 美化版 · 综合看板 (B6) · v2 +差异: 暗色 #0f172a → 浅米黄 #FAF6EE + 字体更大、卡片阴影更深、图表化进度条 +数据: 复用 06-08 三个清洗产物(validation_report/tracking/revenue) +""" +import json, csv, urllib.request, urllib.error, re +from pathlib import Path +from datetime import date + +import sys +DATA_DATE = sys.argv[1] if len(sys.argv) > 1 else '2026-06-08' +BASE = Path('/mnt/y/Openclaw_Hub/03.资源/实施项目 wiki/dashboard/data') / DATA_DATE / 'cleaned' +TODAY = date.today().strftime('%Y-%m-%d') + +# ── 调 GLM 生成 CSS 主题 ── +def call_glm(prompt, max_tokens=1500): + with open('/home/taolm/.hermes/.env') as f: + for line in f: + if line.startswith('GLM_API_KEY'): + key = line.split('=', 1)[1].strip() + break + data = json.dumps({ + 'model': 'glm-4-flash', + 'messages': [{'role': 'user', 'content': prompt}], + 'max_tokens': max_tokens, 'temperature': 0.7, + }).encode('utf-8') + # 试 2 次, 给足 60s + for attempt in range(2): + try: + req = urllib.request.Request( + 'https://open.bigmodel.cn/api/paas/v4/chat/completions', + data=data, + headers={'Authorization': f'Bearer {key}', 'Content-Type': 'application/json'}, + method='POST' + ) + r = urllib.request.urlopen(req, timeout=60) + return json.loads(r.read())['choices'][0]['message']['content'] + except Exception as e: + print(f' ⚠️ GLM 第{attempt+1}次失败: {str(e)[:60]}') + if attempt == 0: + import time; time.sleep(2) + return None + +print('🎨 调用 GLM 设计综合看板 CSS 主题...') +css_prompt = """你是中国港湾中东区域公司技术部的高级视觉设计师。请为一个"技术部月报台账综合看板"设计 CSS 主题。 + +【强制类名 - 必须使用,不可改名】 +- body: 整页背景 #FAF6EE (浅米黄), 字体 'Microsoft YaHei', 'PingFang SC' +- .header: 居中, 底边 3px solid #C8962E 金色 +- .header h1: 32px, 颜色 #1A3A5C +- .header p: 14px, 颜色 #8B7355 +- .grid: CSS Grid repeat(3, 1fr), gap 20px +- .card: 白底, 圆角 12px, padding 24px, 阴影 0 2px 12px rgba(26,58,92,.08) +- .card h2: 18px #1A3A5C, 底边 1px solid #EFE9D9 +- .metric: flex 横向, padding 8px 0, 底边 1px solid #EFE9D9 +- .metric .label: 14px #5C7A99 +- .metric .value: 18px 粗体 #1A3A5C +- .value.good: 颜色 #2E7D32 (成功) +- .value.warn: 颜色 #C8962E (警告) +- .value.bad: 颜色 #D94E34 (错误) +- .table-wrap: 白底卡片, 圆角 12px, padding 24px +- .table-wrap h2: 18px #1A3A5C, 底边 2px solid #C8962E +- th: 背景 #1A3A5C 白字 14px +- td: padding 8px, 底边 1px solid #EFE9D9 +- tr:nth-child(even) td: 斑马纹 #F5F1E8 +- tr:hover td: 高亮 #FFF8E1 +- .bar: 高度 10px, 渐变 linear-gradient(90deg, #1A3A5C, #C8962E) +- .bar-bg: 高度 10px, 灰底 #EFE9D9 +- .red / .orange / .green: 文字色 #D94E34 / #C8962E / #2E7D32 +- .num: 右对齐, tabular-nums +- .footer: 居中, 12px, #8B7355 +- @media (max-width: 900px) { .grid { grid-template-columns: 1fr; } } + +【输出要求】 +只输出 CSS 规则, 不要写 HTML body, 不要 markdown 围栏, 不要解释, 直接写 CSS。 +开头不要写 ```css 之类的标记。CSS 控制在 70 行以内。""" +css_content = call_glm(css_prompt) +if not css_content: + css_content = ''' +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; background: #FAF6EE; color: #1A3A5C; padding: 20px; } +.header { text-align: center; padding: 30px 0; border-bottom: 3px solid #C8962E; margin-bottom: 30px; } +.header h1 { color: #1A3A5C; font-size: 32px; } +.header p { color: #8B7355; margin-top: 8px; font-size: 14px; } +.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 30px; } +.card { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 12px rgba(26,58,92,.08); } +.card h2 { color: #1A3A5C; font-size: 18px; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #EFE9D9; } +.metric { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #EFE9D9; } +.metric .label { color: #5C7A99; font-size: 14px; } +.metric .value { font-weight: bold; font-size: 20px; color: #1A3A5C; } +.value.good { color: #2E7D32; } +.value.warn { color: #C8962E; } +.value.bad { color: #D94E34; } +.table-wrap { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 12px rgba(26,58,92,.08); } +.table-wrap h2 { color: #1A3A5C; font-size: 18px; margin-bottom: 15px; padding-bottom: 8px; border-bottom: 2px solid #C8962E; } +table { width: 100%; border-collapse: collapse; font-size: 13px; } +th { background: #1A3A5C; color: #fff; padding: 10px 8px; text-align: left; } +td { padding: 8px; border-bottom: 1px solid #EFE9D9; } +tr:nth-child(even) td { background: #F5F1E8; } +tr:hover td { background: #FFF8E1; } +.bar { display: inline-block; height: 10px; border-radius: 5px; background: linear-gradient(90deg, #1A3A5C, #C8962E); } +.bar-bg { display: inline-block; height: 10px; border-radius: 5px; background: #EFE9D9; width: 100px; } +.red { color: #D94E34; } .orange { color: #C8962E; } .green { color: #2E7D32; } +.num { text-align: right; font-variant-numeric: tabular-nums; } +.footer { text-align: center; color: #8B7355; padding: 20px; font-size: 12px; } +@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } } +''' + print(' 使用 fallback 主题') +else: + m = re.search(r']*>([\s\S]+?)', css_content) + if not m: + m = re.search(r'```css\s*([\s\S]+?)\s*```', css_content) + if m: + css_content = m.group(1) + else: + css_content = re.sub(r'```[a-z]*\s*', '', css_content) + css_content = re.sub(r'\s*```', '', css_content) + # 后处理 + css_content = re.sub(r'(\d+px)\s+gold\b', r'\1 solid #C8962E', css_content) + css_content = re.sub(r'(\d+px)\s+blue\b', r'\1 solid #1A3A5C', css_content) + if "font-family" not in css_content[:600]: + css_content = "body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; }\n" + css_content + print(f' ✅ GLM 返回 {len(css_content)} 字符 CSS') + +# ── 数据 ── +reports = {} +for name in ['validation_report', 'tracking_validation', 'revenue_validation']: + p = BASE / f'{name}.json' + if p.exists(): + with open(p, encoding='utf-8') as f: + reports[name] = json.load(f) + +methods_ps = [] +with open(BASE / 'project_summary.csv', encoding='utf-8-sig') as f: + methods_ps = list(csv.DictReader(f)) + +tracking_ps = [] +with open(BASE / 'tracking_project_summary.csv', encoding='utf-8-sig') as f: + tracking_ps = list(csv.DictReader(f)) + +revenue_ps = [] +with open(BASE / 'revenue_active.csv', encoding='utf-8-sig') as f: + revenue_ps = list(csv.DictReader(f)) + +m = reports.get('validation_report', {}).get('summary', {}) +t = reports.get('tracking_validation', {}) +r = reports.get('revenue_validation', {}) + +def fmt_money(v): + try: return f'${float(v)/10000:,.0f}万' + except: return str(v) + +def fmt_pct(num, den): + try: return f'{int(num)/int(den)*100:.0f}%' + except: return 'N/A' + +# Build project ranking (top 15 by 方案总数) +def parse_int(v): + try: return int(str(v).replace(',', '').replace('%', '')) + except: return 0 + +methods_top = sorted(methods_ps, key=lambda x: -parse_int(x.get('方案总数', 0)))[:15] +methods_rows = [] +for row in methods_top: + total = parse_int(row.get('方案总数', 0)) + done = parse_int(row.get('已审批', 0)) + over = parse_int(row.get('超规', 0)) + rate = parse_int(str(row.get('审批率', '0')).rstrip('%')) + bar_w = max(rate, 4) if total else 0 + methods_rows.append( + f'{row.get("项目名称","")}{total}{done}' + f'{over}' + f'
' + f'{rate}%{row.get("国别","")}' + ) +methods_html = '\n'.join(methods_rows) + +# Tracking overdue top 15 +tracking_top = sorted(tracking_ps, key=lambda x: -parse_int(x.get('逾期', 0)))[:15] +tracking_rows = [] +for row in tracking_top: + overdue = parse_int(row.get('逾期', 0)) + total = parse_int(row.get('总任务数', 0)) + done = parse_int(row.get('已完成', 0)) + rate = parse_int(str(row.get('完成率', '0')).rstrip('%')) + if overdue == 0 and total < 5: continue + cls = 'red' if overdue > 50 else 'orange' if overdue > 20 else 'green' + tracking_rows.append( + f'{row.get("项目名称","")}{total}{done}' + f'{overdue}{rate}%' + ) +tracking_html = '\n'.join(tracking_rows[:15]) if tracking_rows else '无逾期数据' + +# Revenue top 10 +revenue_top = sorted([row for row in revenue_ps if parse_int(row.get('本年营业收入_num', 0)) > 0], + key=lambda x: -parse_int(x.get('本年营业收入_num', 0)))[:10] +revenue_rows = [] +for row in revenue_top: + name = str(row.get('项目名称', row.get('地区/驻外机构/项目', '')))[:50] + annual = parse_int(row.get('本年营业收入_num', 0)) + contract = parse_int(row.get('签约合同额中国港湾占额(不含税)_num', 0)) + cumulative = parse_int(row.get('项目累计营业收入_num', 0)) + revenue_rows.append( + f'{name}${contract/10000:,.0f}' + f'${annual/10000:,.0f}' + f'${cumulative/10000:,.0f}' + ) +revenue_html = '\n'.join(revenue_rows) if revenue_rows else '暂无营收数据' + +approval_pct = m.get('审批率', 'N/A') +approval_cls = 'good' if str(approval_pct).isdigit() and int(approval_pct) >= 80 else 'warn' + +html = f''' + + + + +中东区域公司 技术部月报台账看板 · GLM 美化版 | {DATA_DATE} + + + + +
+

🏗️ 中东区域公司 · 技术部月报台账看板

+

数据日期: {DATA_DATE} | 生成时间: {TODAY} | GLM-4-Flash 美化版

+
+ +
+
+

📋 危大方案编审进度

+
有效方案总数{m.get('有效方案','N/A')}
+
超一定规模{m.get('超规','N/A')}
+
已审批{m.get('已审批','N/A')}
+
审批率{approval_pct}
+
预警信号{m.get('预警',0)}
+
覆盖项目{m.get('项目','N/A')}
+
+ +
+

🔄 项目启动动态跟踪

+
跟踪项目数{t.get('me_projects','N/A')}
+
总任务数{t.get('total_tasks','N/A')}
+
已完成{t.get('completed','N/A')}
+
逾期{t.get('overdue','N/A')}
+
完成率{fmt_pct(t.get('completed',0), t.get('total_tasks',1))}
+
+ +
+

💰 营业额与产值

+
中东项目数{r.get('all_me','N/A')}
+
活跃在建{r.get('active','N/A')}
+
签约合同额{fmt_money(r.get('total_contract',0))}
+
本年营业收入{fmt_money(r.get('annual_revenue',0))}
+
累计营业收入{fmt_money(r.get('cumulative_revenue',0))}
+
+
+ +
+

📋 危大方案编审 — 项目排行 Top 15

+ + +{methods_html} +
项目名称方案已审批超规审批进度审批率国别
+
+ +
+

🔄 项目启动跟踪 — 逾期项目 Top 15

+ + +{tracking_html} +
项目名称总任务已完成逾期完成率
+
+ +
+

💰 营收项目 Top 10(本年营收排序)

+ + +{revenue_html} +
项目名称签约合同额(万美元)本年营收累计营收
+
+ + + +''' + +out = BASE / 'dashboard_glm.html' +out.write_text(html, encoding='utf-8') +print(f'✅ {out}') +print(f' {len(html):,} bytes') diff --git a/src/b4b_certified_dashboard_glm.py b/src/b4b_certified_dashboard_glm.py new file mode 100644 index 0000000..ae9ccec --- /dev/null +++ b/src/b4b_certified_dashboard_glm.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +""" +GLM 美化版 · 危大方案独立看板 (b4b) · v2 +差异: 不再走 glb 暗色系,改走 GLM 设计的"中国港湾蓝金商务风"主题 + - 浅米黄底(#FAF6EE)+ 深蓝主调(#1A3A5C)+ 金色点缀(#C8962E) + - 大字号 KPI + 渐变进度条 + 圆角卡片阴影 + - 移动响应式 +数据: 复用 b1_methods 清洗后的 parquet, 不重算 +""" +import pandas as pd, warnings, json, os, urllib.request, urllib.error +from pathlib import Path +warnings.filterwarnings('ignore') + +import sys +DATA_DATE = sys.argv[1] if len(sys.argv) > 1 else '2026-06-08' +BASE = Path(f'/mnt/y/Openclaw_Hub/03.资源/实施项目 wiki/dashboard/data/{DATA_DATE}/cleaned') + +# ── 调 GLM 生成 CSS 主题 ── +def call_glm(prompt: str, max_tokens: int = 2000) -> str: + """调用 glm-4-flash, 失败 fallback 默认 CSS""" + with open('/home/taolm/.hermes/.env') as f: + for line in f: + if line.startswith('GLM_API_KEY'): + key = line.split('=', 1)[1].strip() + break + data = json.dumps({ + 'model': 'glm-4-flash', + 'messages': [{'role': 'user', 'content': prompt}], + 'max_tokens': max_tokens, + 'temperature': 0.7, + }).encode('utf-8') + try: + req = urllib.request.Request( + 'https://open.bigmodel.cn/api/paas/v4/chat/completions', + data=data, + headers={'Authorization': f'Bearer {key}', 'Content-Type': 'application/json'}, + method='POST' + ) + r = urllib.request.urlopen(req, timeout=30) + return json.loads(r.read())['choices'][0]['message']['content'] + except Exception as e: + print(f' ⚠️ GLM 调用失败: {e}') + return None + +print('🎨 调用 GLM 设计 CSS 主题...') +css_prompt = """你是中国港湾中东区域公司技术部的高级视觉设计师。请为一个"危大方案编审进度看板"设计 CSS 主题。 + +【强制要求 - 必须使用以下类名,不可改名】 +- body (整页背景 #FAF6EE 浅米黄) +- .header h1 / .header p (标题区, 32px 主色 #1A3A5C) +- .kpi-grid (CSS Grid, repeat(4, 1fr), gap 20px) +- .kpi (卡片, 圆角 12px, 阴影 0 4px 16px rgba(26,58,92,.08), 顶 4px 金色边) +- .kpi .num (48px 粗体深蓝) +- .kpi .label (14px 灰 #5C7A99) +- .table-wrap (白底卡片, 圆角 12px, 内边距 24px) +- .table-wrap h2 (22px 深蓝, 底边 2px 金色) +- table / th / td / tr +- th (深蓝 #1A3A5C 底白字) +- tr:nth-child(even) td (斑马纹 #F5F1E8) +- tr:hover td (高亮 #FFF8E1) +- .badge-ok (绿底 #2E7D32, 圆角 12px) +- .badge-warn (金底 #C8962E) +- .badge-alert (红底 #D94E34) +- .num (右对齐, tabular-nums) +- .bar-bg (灰底 #EFE9D9 圆角 5px 高度 10px) +- .bar (蓝→金渐变 linear-gradient(90deg,#1A3A5C,#C8962E), 高度 10px) +- @media (max-width: 768px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } } + +【输出要求】 +只输出 或 ```css...``` 块 + import re + m = re.search(r']*>([\s\S]+?)', css_content) + if not m: + m = re.search(r'```css\s*([\s\S]+?)\s*```', css_content) + if m: + css_content = m.group(1) + else: + css_content = re.sub(r'```[a-z]*\s*', '', css_content) + css_content = re.sub(r'\s*```', '', css_content) + # ── 鲁棒性后处理 ── + # 1. 补 body 字体(若 GLM 漏) + if "font-family" not in css_content.split("body")[1][:200] if "body" in css_content else True: + css_content += "\nbody { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; color: #1A3A5C; padding: 24px; }" + # 2. 修语法错(4px gold → 4px solid #C8962E) + css_content = re.sub(r'(\d+px)\s+gold\b', r'\1 solid #C8962E', css_content) + css_content = re.sub(r'(\d+px)\s+blue\b', r'\1 solid #1A3A5C', css_content) + # 3. 兜底: 必备基础样式 + must_have = { + "body { font-family": "body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; color: #1A3A5C; padding: 24px; }", + "th {": "th { background: #1A3A5C; color: #fff; padding: 12px 10px; text-align: left; font-size: 14px; }", + } + for marker, inject in must_have.items(): + if marker not in css_content: + css_content += "\n" + inject + print(f' ✅ GLM 返回 {len(css_content)} 字符 CSS (后处理完毕)') + +# ── 数据抽取 ── +df = pd.read_parquet(BASE / 'methods_cleaned.parquet') +valid = df[df['是否有效登记'] == True].copy() +valid['开工年份'] = pd.to_datetime(valid['分部分项工程计划开工日期'], errors='coerce').dt.year +y2026 = valid[valid['开工年份'] >= 2026].copy() + +def simple_status(s): + s = str(s) + if any(kw in s for kw in ['已审批', '已备案']): + return '已完成' + return '未完成' + +y2026['简化状态'] = y2026['方案状态_clean'].apply(simple_status) +completed = (y2026['简化状态'] == '已完成').sum() +total = len(y2026) +oversized = (y2026['是否超一定规模'].astype(str) == '是').sum() +y2026['预警信号'] = y2026['预警信号'].fillna('none') +red_n = (y2026['预警信号'].astype(str) == 'red').sum() +orange_n = (y2026['预警信号'].astype(str) == 'orange').sum() +unfinished_n = (y2026['简化状态'] == '未完成').sum() +approved_pct = round(completed / total * 100) if total else 0 + +# By country +country_stats = y2026.groupby('所属国别').agg( + 项目数=('项目名称', 'nunique'), + 方案总数=('方案名称', 'count'), + 已完成=('简化状态', lambda x: (x == '已完成').sum()), + 超规=('是否超一定规模', lambda x: (x.astype(str) == '是').sum()), +).reset_index() +country_stats['审批率'] = (country_stats['已完成'] / country_stats['方案总数'] * 100).round(0).astype(int) + +# Project summary +ps = y2026.groupby('项目名称').agg( + 方案总数=('方案名称', 'count'), + 已完成=('简化状态', lambda x: (x == '已完成').sum()), + 超规=('是否超一定规模', lambda x: (x.astype(str) == '是').sum()), +).reset_index() +ps['未完成'] = ps['方案总数'] - ps['已完成'] +ps['审批率'] = (ps['已完成'] / ps['方案总数'] * 100).round(0).astype(int) +ps['国别'] = ps['项目名称'].map(y2026.groupby('项目名称')['所属国别'].first()) +ps = ps.sort_values('方案总数', ascending=False) + +# ── 生成 HTML ── +def rate_badge(rate): + cls = 'badge-ok' if rate >= 80 else 'badge-warn' if rate >= 50 else 'badge-alert' + return f'{rate}%' + +def rate_bar(rate, total_n): + width = max(int(rate), 4) if total_n else 0 + return f'
' + +project_rows = [] +for _, r in ps.iterrows(): + name = str(r['项目名称']) + t = int(r['方案总数']); d = int(r['已完成']); s = int(r['超规']); rate = int(r['审批率']) + project_rows.append( + f'{name}{t}{d}' + f'{s}{rate_bar(rate, t)}' + f'{rate_badge(rate)}{r.get("国别","")}' + ) +projects_html = '\n'.join(project_rows) + +country_rows = [] +for _, r in country_stats.iterrows(): + name = str(r['所属国别']) + proj_n = int(r['项目数']); t = int(r['方案总数']); d = int(r['已完成']); s = int(r['超规']); c = int(r['审批率']) + country_rows.append( + f'{name}{proj_n}{t}' + f'{d}{s}{rate_badge(c)}' + ) +country_html = '\n'.join(country_rows) + +# 预警明细 (橙色/红色) +warnings_df = y2026[y2026['预警信号'].astype(str).isin(['red', 'orange'])].copy() +warning_rows = [] +for _, r in warnings_df.iterrows(): + warning_rows.append( + f'{r["项目名称"]}{r["方案名称"][:50]}...' + f'{r["所属国别"]}{r["分部分项工程计划开工日期"]}' + f'{r["预警信号"]}' + ) +warning_html = '\n'.join(warning_rows) if warning_rows else '🟢 当前无预警方案' + +html = f''' + + + + +危大方案编审进度看板 · GLM 美化版 | 中东区域公司 + + + + +
+

🏗️ 危大方案编审进度看板

+

2026 年开工项目 · 中东区域公司 · 数据日期 {DATA_DATE} · GLM 美化版

+
+ + +
+
方案总数
{total}
+
已审批
{completed}
+
审批率
{approved_pct}%
+
超规方案
{oversized}
+
未完成
{unfinished_n}
+
橙色预警
{orange_n}
+
红色预警
{red_n}
+
覆盖项目
{len(ps)}
+
+ + +
+

⚠️ 预警方案明细

+ + +{warning_html} +
项目名称方案名称国别计划开工预警等级
+
+ + +
+

📋 项目明细(按方案总数排序)

+ + +{projects_html} + + + + +
项目名称方案已审批超规进度审批率国别
合计{total}{completed}{oversized}{rate_bar(approved_pct, total)}{rate_badge(approved_pct)}
+
+ + +
+

🌍 按国别汇总

+ + +{country_html} +
国别项目数方案总数已审批超规审批率
+
+ +
+

中国港湾中东区域公司 技术部 · GLM-4-Flash 美化版 · 自动生成于 {DATA_DATE}

+
+ +''' + +out = BASE / 'certified_schemes_dashboard_glm.html' +out.write_text(html, encoding='utf-8') +print(f'✅ {out}') +print(f' {len(html):,} bytes') +print(f' {total} schemes, {len(ps)} projects, {approved_pct}% completion') diff --git a/src/gen_pptx_glm.py b/src/gen_pptx_glm.py new file mode 100644 index 0000000..2962ae0 --- /dev/null +++ b/src/gen_pptx_glm.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +""" +GLM 美化版 · 危大方案编审进度看板 PPTX · v2 +差异 vs 原版 gen_pptx.py: + - 浅米黄底 (#FAF6EE) 替代暗色 + - 蓝金渐变标题条替代纯色蓝条 + - 卡片圆角+阴影, 加大留白 + - KPI 字号 44→52 + - 国别图改为横向进度条 + - 预警颜色用约定橙/红 (#D94E34 / #C8962E) +数据: 复用 06-08 三个清洗产物, 不重算 +""" +import json, csv, urllib.request, urllib.error, urllib.parse +from pathlib import Path +from datetime import date + +from pptx import Presentation +from pptx.util import Inches, Pt, Cm, Emu +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN +from pptx.enum.shapes import MSO_SHAPE +from pptx.chart.data import CategoryChartData +from pptx.enum.chart import XL_CHART_TYPE + +import sys +DATA_DATE = sys.argv[1] if len(sys.argv) > 1 else '2026-06-08' +BASE = Path(f'/mnt/y/Openclaw_Hub/03.资源/实施项目 wiki/dashboard/data/{DATA_DATE}/cleaned') +TODAY = date.today().strftime('%Y-%m-%d') + +# ── GLM 美化版品牌色 (浅米黄蓝金商务风) ── +BG_PAGE = RGBColor(0xFA, 0xF6, 0xEE) # 整页米黄 +BG_CARD = RGBColor(0xFF, 0xFF, 0xFF) # 卡片白 +BLUE = RGBColor(0x1A, 0x3A, 0x5C) # 主色 +GOLD = RGBColor(0xC8, 0x96, 0x2E) # 强调 +RED = RGBColor(0xD9, 0x4E, 0x34) # 警示 +GREEN = RGBColor(0x2E, 0x7D, 0x32) # 成功 +ORANGE = RGBColor(0xF0, 0x80, 0x00) # 橙色预警 +GRAY = RGBColor(0x5C, 0x7A, 0x99) # 次文字 +LIGHT = RGBColor(0xEF, 0xE9, 0xD9) # 浅米黄 +ZEBRA = RGBColor(0xF5, 0xF1, 0xE8) # 斑马纹 +BLACK = RGBColor(0x33, 0x33, 0x33) +WHITE = RGBColor(0xFF, 0xFF, 0xFF) + +prs = Presentation() +prs.slide_width = Inches(13.333) +prs.slide_height = Inches(7.5) + +# ── 形状辅助 ── +def R(l, t, w, h, fill=None, line=None, line_w=0.5, radius_pct=None): + s = prs.slides[-1].shapes.add_shape(MSO_SHAPE.RECTANGLE, l, t, w, h) + s.line.fill.background() + if fill: + s.fill.solid(); s.fill.fore_color.rgb = fill + if line: + s.line.color.rgb = line; s.line.width = Pt(line_w) + return s + +def T(l, t, w, h, txt, sz=12, clr=BLACK, b=False, fn='Microsoft YaHei', al=PP_ALIGN.LEFT): + tb = prs.slides[-1].shapes.add_textbox(l, t, w, h) + tb.text_frame.word_wrap = True + p = tb.text_frame.paragraphs[0]; p.text = txt + p.font.size = Pt(sz); p.font.color.rgb = clr; p.font.bold = b + p.font.name = fn; p.alignment = al + return tb + +def kpi_card(l, t, w, h, num, lbl, nc=BLUE, ns=52, ls=11): + """白底大字号 KPI 卡片, 顶 4px 金色边""" + # 卡底 + R(l, t, w, h, fill=BG_CARD, line=LIGHT, line_w=0.5) + # 顶金色边 + R(l, t, w, Inches(0.06), fill=GOLD) + # 数字 + T(l, t + Inches(0.18), w, Inches(0.8), str(num), sz=ns, clr=nc, b=True, al=PP_ALIGN.CENTER) + # 标签 + T(l, t + Inches(1.05), w, Inches(0.3), lbl, sz=ls, clr=GRAY, al=PP_ALIGN.CENTER) + +def donut(l, t, w, h, vals, labels, colors): + cd = CategoryChartData() + cd.categories = labels + cd.add_series('', vals) + ch = prs.slides[-1].shapes.add_chart(XL_CHART_TYPE.DOUGHNUT, l, t, w, h, cd) + plot = ch.chart.plots[0] + for i, c in enumerate(colors): + plot.series[0].points[i].format.fill.solid() + plot.series[0].points[i].format.fill.fore_color.rgb = c + ch.chart.has_legend = False + ch.chart.has_title = False + return ch + +def hbar(l, t, w, h, name, val, max_val, color): + """横向进度条(国别占比)""" + # 名称 + T(l, t, Inches(1.7), h, name, sz=11, clr=BLACK, al=PP_ALIGN.RIGHT) + # 灰底 + R(l + Inches(1.8), t + Inches(0.05), w - Inches(2.5), Inches(0.2), fill=LIGHT) + # 实际条 + bar_w = (w - Inches(2.5)) * (val / max_val if max_val else 0) + if bar_w > 0: + R(l + Inches(1.8), t + Inches(0.05), bar_w, Inches(0.2), fill=color) + # 数值 + T(l + w - Inches(0.6), t, Inches(0.55), h, str(val), sz=11, clr=BLUE, b=True, al=PP_ALIGN.RIGHT) + +# ═══════════════════════════════════════ +# SLIDE 1 · 关键指标 +# ═══════════════════════════════════════ +s1 = prs.slides.add_slide(prs.slide_layouts[6]) +# 整页底色 +R(Inches(0), Inches(0), Inches(13.333), Inches(7.5), fill=BG_PAGE) +# 顶部金边导航 +R(Inches(0), Inches(0), Inches(13.333), Inches(0.06), fill=GOLD) +R(Inches(0), Inches(0.06), Inches(13.333), Inches(0.5), fill=BLUE) +T(Inches(0.5), Inches(0.16), Inches(3), Inches(0.3), '★ 关键指标', sz=12, clr=GOLD, b=True) +T(Inches(3.5), Inches(0.16), Inches(3), Inches(0.3), '国别分布', sz=11, clr=WHITE) +T(Inches(6.5), Inches(0.16), Inches(3), Inches(0.3), '预警明细', sz=11, clr=WHITE) +# 右上 GLM 标识 +T(Inches(10.5), Inches(0.16), Inches(2.5), Inches(0.3), '🎨 GLM 美化版', sz=10, clr=GOLD, b=True, al=PP_ALIGN.RIGHT) + +# 标题区 +T(Inches(0.5), Inches(0.85), Inches(8), Inches(0.5), '🏗️ 危大方案编审进度看板', sz=28, clr=BLUE, b=True) +T(Inches(0.5), Inches(1.35), Inches(8), Inches(0.3), f'2026 年开工项目 · 中东区域公司 · 数据日期 {DATA_DATE}', + sz=12, clr=GRAY) +# 标题右侧块(强调) +R(Inches(10.5), Inches(0.85), Inches(2.3), Inches(0.8), fill=GOLD) +T(Inches(10.5), Inches(0.95), Inches(2.3), Inches(0.6), f'数据日期\n{DATA_DATE}', sz=11, clr=WHITE, b=True, al=PP_ALIGN.CENTER) + +# ── KPI 行 (4 个) ── +# 数据 +reports = {} +for name in ['validation_report', 'tracking_validation', 'revenue_validation']: + p = BASE / f'{name}.json' + if p.exists(): + with open(p, encoding='utf-8') as f: + reports[name] = json.load(f) +m_sum = reports.get('validation_report', {}).get('summary', {}) + +KW = Inches(3.0); KH = Inches(1.55); GAP = Inches(0.2) +KY = Inches(1.95) +kx0 = Inches(0.5) +for i, (num, lbl, clr) in enumerate([ + (m_sum.get('有效方案', 'N/A'), '有效方案总数', BLUE), + (m_sum.get('已审批', 'N/A'), '已审批(83%目标)', GREEN), + (m_sum.get('超规', 'N/A'), '超一定规模方案', GOLD), + (m_sum.get('预警', 0), '预警信号', RED if m_sum.get('预警', 0) > 0 else GREEN), +]): + kx = kx0 + (KW + GAP) * i + kpi_card(kx, KY, KW, KH, num, lbl, nc=clr, ns=44, ls=11) + +# ── 国别 + 饼图 (左 2/3) ── +R(Inches(0.5), Inches(3.7), Inches(8.2), Inches(3.0), fill=BG_CARD, line=LIGHT, line_w=0.5) +R(Inches(0.5), Inches(3.7), Inches(8.2), Inches(0.4), fill=BLUE) +T(Inches(0.7), Inches(3.76), Inches(6), Inches(0.3), '🌍 按国别分布 · 方案数 + 一般/超规', sz=12, clr=WHITE, b=True) + +# 国别数据 +methods_ps = [] +with open(BASE / 'project_summary.csv', encoding='utf-8-sig') as f: + methods_ps = list(csv.DictReader(f)) +country_map = {} +for row in methods_ps: + cn = row.get('国别', '') + if not cn: continue + if cn not in country_map: + country_map[cn] = [0, 0] # [一般, 超规] + country_map[cn][0] += int(row.get('已审批', 0) or 0) + country_map[cn][1] += int(row.get('超规', 0) or 0) +# 按总方案数排序 +country_sorted = sorted(country_map.items(), key=lambda x: -(x[1][0] + x[1][1])) +max_total = max((v[0] + v[1] for v in country_map.values()), default=1) + +# 画横向条形 +bx = Inches(0.5); by = Inches(4.25); bw = Inches(8.2); bh_each = Inches(0.45) +for i, (name, (gn, ch)) in enumerate(country_sorted[:5]): + y = by + bh_each * i + hbar(bx, y, bw, bh_each, name, gn + ch, max_total, BLUE) + # 一般 vs 超规 副条 + inner_w = Inches(7.0) * ((gn + ch) / max_total) + if ch > 0 and gn + ch > 0: + gold_w = inner_w * (ch / (gn + ch)) + R(bx + Inches(1.8) + inner_w - gold_w, y + Inches(0.27), gold_w, Inches(0.12), fill=GOLD) + # 标签 + T(bx + Inches(0.05), y + Inches(0.27), Inches(0.5), Inches(0.12), '一般', sz=7, clr=BLUE, al=PP_ALIGN.LEFT) + T(bx + Inches(1.7), y + Inches(0.27), Inches(0.5), Inches(0.12), '超规', sz=7, clr=GOLD, al=PP_ALIGN.LEFT) + +# 图例 +T(Inches(0.7), Inches(6.4), Inches(2), Inches(0.2), '■ 一般类方案', sz=9, clr=BLUE, b=True) +T(Inches(2.5), Inches(6.4), Inches(2), Inches(0.2), '■ 超规类方案', sz=9, clr=GOLD, b=True) + +# ── 右侧: 饼图 + 审批率 ── +R(Inches(8.9), Inches(3.7), Inches(3.95), Inches(3.0), fill=BG_CARD, line=LIGHT, line_w=0.5) +R(Inches(8.9), Inches(3.7), Inches(3.95), Inches(0.4), fill=BLUE) +T(Inches(9.1), Inches(3.76), Inches(3.5), Inches(0.3), '📊 方案分类占比', sz=12, clr=WHITE, b=True) + +# 饼图 +total_general = sum(v[0] for v in country_map.values()) +total_oversized = sum(v[1] for v in country_map.values()) +if total_general + total_oversized > 0: + donut(Inches(9.0), Inches(4.15), Inches(2.0), Inches(2.0), + [total_general, total_oversized], ['一般类', '超规类'], [BLUE, GOLD]) + +# 饼图右侧说明 +T(Inches(11.1), Inches(4.3), Inches(1.7), Inches(0.3), f'一般类', sz=11, clr=BLUE, b=True) +T(Inches(11.1), Inches(4.55), Inches(1.7), Inches(0.4), f'{total_general} 项', sz=18, clr=BLUE, b=True) +T(Inches(11.1), Inches(5.05), Inches(1.7), Inches(0.3), f'超规类', sz=11, clr=GOLD, b=True) +T(Inches(11.1), Inches(5.3), Inches(1.7), Inches(0.4), f'{total_oversized} 项', sz=18, clr=GOLD, b=True) + +# 审批率进度条 +T(Inches(8.95), Inches(6.25), Inches(2), Inches(0.2), '审批完成率', sz=9, clr=GRAY) +approved = int(m_sum.get('已审批', 0) or 0) +total = int(m_sum.get('有效方案', 1) or 1) +rate = int(approved / total * 100) if total else 0 +R(Inches(8.95), Inches(6.45), Inches(3.0), Inches(0.15), fill=LIGHT) +R(Inches(8.95), Inches(6.45), Inches(3.0 * rate / 100), Inches(0.15), fill=BLUE) +T(Inches(11.5), Inches(6.25), Inches(1.3), Inches(0.4), f'{rate}%', sz=14, clr=BLUE, b=True, al=PP_ALIGN.RIGHT) + +# 页脚 +T(Inches(0.5), Inches(7.05), Inches(6), Inches(0.25), + '中国港湾中东区域公司 技术部 · GLM-4-Flash 美化版 · 数据源: OA 项管平台', sz=8, clr=GRAY) +T(Inches(11.5), Inches(7.05), Inches(1.3), Inches(0.25), '1 / 2', sz=8, clr=GRAY, al=PP_ALIGN.RIGHT) + +# ═══════════════════════════════════════ +# SLIDE 2 · 预警明细 + 项目排行 +# ═══════════════════════════════════════ +s2 = prs.slides.add_slide(prs.slide_layouts[6]) +R(Inches(0), Inches(0), Inches(13.333), Inches(7.5), fill=BG_PAGE) +R(Inches(0), Inches(0), Inches(13.333), Inches(0.06), fill=GOLD) +R(Inches(0), Inches(0.06), Inches(13.333), Inches(0.5), fill=BLUE) +T(Inches(0.5), Inches(0.16), Inches(3), Inches(0.3), '关键指标', sz=11, clr=WHITE) +T(Inches(3.5), Inches(0.16), Inches(3), Inches(0.3), '国别分布', sz=11, clr=WHITE) +T(Inches(6.5), Inches(0.16), Inches(3), Inches(0.3), '★ 预警明细', sz=12, clr=GOLD, b=True) +T(Inches(10.5), Inches(0.16), Inches(2.5), Inches(0.3), '🎨 GLM 美化版', sz=10, clr=GOLD, b=True, al=PP_ALIGN.RIGHT) + +# 标题 +T(Inches(0.5), Inches(0.85), Inches(8), Inches(0.5), '⚠️ 预警方案 + 项目排行', sz=28, clr=BLUE, b=True) +T(Inches(0.5), Inches(1.35), Inches(8), Inches(0.3), f'按审批紧迫度排序 · {TODAY} 生成', + sz=12, clr=GRAY) + +# 预警明细表 +import pandas as pd +df_methods = pd.read_parquet(BASE / 'methods_cleaned.parquet') +valid_df = df_methods[df_methods['是否有效登记'] == True].copy() +valid_df['开工年份'] = pd.to_datetime(valid_df['分部分项工程计划开工日期'], errors='coerce').dt.year +warn_df = valid_df[valid_df['预警信号'].astype(str).isin(['red', 'orange'])].copy() + +# 预警块标题 +R(Inches(0.5), Inches(1.85), Inches(12.3), Inches(0.4), fill=BLUE) +T(Inches(0.7), Inches(1.91), Inches(8), Inches(0.3), f'🚨 预警方案明细 ({len(warn_df)} 项)', + sz=12, clr=WHITE, b=True) + +# 预警表 +if len(warn_df) > 0: + tbl = s2.shapes.add_table(min(len(warn_df) + 1, 8), 5, + Inches(0.5), Inches(2.25), Inches(12.3), Inches(2.0)).table + tbl.columns[0].width = Inches(3.5) + tbl.columns[1].width = Inches(5.0) + tbl.columns[2].width = Inches(1.5) + tbl.columns[3].width = Inches(1.3) + tbl.columns[4].width = Inches(1.0) + for i, h in enumerate(['项目名称', '方案名称', '国别', '计划开工', '预警等级']): + c = tbl.cell(0, i); c.text = h + c.fill.solid(); c.fill.fore_color.rgb = GOLD + for p in c.text_frame.paragraphs: + p.font.size = Pt(10); p.font.bold = True + p.font.color.rgb = WHITE; p.font.name = 'Microsoft YaHei' + p.alignment = PP_ALIGN.CENTER + for i, (_, r) in enumerate(warn_df.head(7).iterrows(), 1): + level = str(r.get('预警信号', '')) + row_data = [ + str(r.get('项目名称', ''))[:30], + str(r.get('方案名称', ''))[:40], + str(r.get('所属国别', '')), + str(r.get('分部分项工程计划开工日期', '')), + level, + ] + for j, val in enumerate(row_data): + c = tbl.cell(i, j); c.text = val + c.fill.solid(); c.fill.fore_color.rgb = WHITE if i % 2 == 0 else ZEBRA + for p in c.text_frame.paragraphs: + p.font.size = Pt(9) + p.font.name = 'Microsoft YaHei' + p.alignment = PP_ALIGN.CENTER if j > 0 else PP_ALIGN.LEFT + if j == 4: + p.font.color.rgb = RED if level == 'red' else ORANGE + p.font.bold = True + else: + p.font.color.rgb = BLACK + +# 项目排行表 +R(Inches(0.5), Inches(4.4), Inches(12.3), Inches(0.4), fill=BLUE) +T(Inches(0.7), Inches(4.46), Inches(8), Inches(0.3), '📊 方案数 Top 10 项目', + sz=12, clr=WHITE, b=True) + +top10 = sorted(methods_ps, key=lambda x: -int(x.get('方案总数', 0) or 0))[:10] +tbl2 = s2.shapes.add_table(min(len(top10) + 1, 11), 6, + Inches(0.5), Inches(4.8), Inches(12.3), Inches(2.1)).table +tbl2.columns[0].width = Inches(0.5) +tbl2.columns[1].width = Inches(4.3) +tbl2.columns[2].width = Inches(1.3) +tbl2.columns[3].width = Inches(1.3) +tbl2.columns[4].width = Inches(1.3) +tbl2.columns[5].width = Inches(3.6) + +for i, h in enumerate(['#', '项目名称', '方案数', '已审批', '超规', '审批进度']): + c = tbl2.cell(0, i); c.text = h + c.fill.solid(); c.fill.fore_color.rgb = GOLD + for p in c.text_frame.paragraphs: + p.font.size = Pt(10); p.font.bold = True + p.font.color.rgb = WHITE; p.font.name = 'Microsoft YaHei' + p.alignment = PP_ALIGN.CENTER +for i, row in enumerate(top10, 1): + name = str(row.get('项目名称', ''))[:30] + tot = int(row.get('方案总数', 0) or 0) + done = int(row.get('已审批', 0) or 0) + over = int(row.get('超规', 0) or 0) + rate_v = int(str(row.get('审批率', '0')).rstrip('%') or 0) + cells = [str(i), name, str(tot), str(done), str(over), f'{rate_v}%'] + for j, val in enumerate(cells): + c = tbl2.cell(i, j); c.text = val + c.fill.solid(); c.fill.fore_color.rgb = WHITE if i % 2 == 0 else ZEBRA + for p in c.text_frame.paragraphs: + p.font.size = Pt(9) + p.font.name = 'Microsoft YaHei' + p.alignment = PP_ALIGN.CENTER if j != 1 else PP_ALIGN.LEFT + p.font.color.rgb = BLUE if j == 0 or j == 5 else BLACK + p.font.bold = j == 0 or j == 5 + +# 页脚 +T(Inches(0.5), Inches(7.05), Inches(6), Inches(0.25), + '中国港湾中东区域公司 技术部 · GLM-4-Flash 美化版', sz=8, clr=GRAY) +T(Inches(11.5), Inches(7.05), Inches(1.3), Inches(0.25), '2 / 2', sz=8, clr=GRAY, al=PP_ALIGN.RIGHT) + +# ── 保存 ── +out = BASE / '危大方案编审进度看板_GLM版.pptx' +prs.save(out) +print(f'✅ {out}') +print(f' {out.stat().st_size:,} bytes') +print(f' 2 slides: 关键指标 / 预警+Top10')