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 0000000..ec2e05d Binary files /dev/null and b/data/2026-06-08/cleaned/危大方案编审进度看板_GLM版.pptx differ 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')