feat: GLM-4-Flash 美化版看板(浅米黄蓝金商务风)

- 新增 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 幻觉
This commit is contained in:
卧龙 2026-06-09 18:14:42 +08:00
parent ecd5914c97
commit d357206130
6 changed files with 1306 additions and 0 deletions

View File

@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>危大方案编审进度看板 · GLM 美化版 | 中东区域公司</title>
<style>
body {
background-color: #FAF6EE;
}
.header h1, .header p {
color: #1A3A5C;
font-size: 32px;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.kpi {
border-radius: 12px;
box-shadow: 0 4px 16px rgba(26,58,92,.08);
margin-top: 4px;
border: 4px solid gold;
}
.kpi .num {
font-size: 48px;
font-weight: bold;
color: #1A3A5C;
}
.kpi .label {
font-size: 14px;
color: #5C7A99;
}
.table-wrap {
background-color: white;
border-radius: 12px;
padding: 24px;
}
.table-wrap h2 {
font-size: 22px;
color: #1A3A5C;
border-bottom: 2px solid gold;
}
th, td {
text-align: left;
}
th {
color: #1A3A5C;
background-color: white;
}
tr:nth-child(even) td {
background-color: #F5F1E8;
}
tr:hover td {
background-color: #FFF8E1;
}
.badge-ok {
background-color: #2E7D32;
border-radius: 12px;
}
.badge-warn {
background-color: #C8962E;
}
.badge-alert {
background-color: #D94E34;
}
.num {
text-align: right;
}
.bar-bg {
background-color: #EFE9D9;
border-radius: 5px;
height: 10px;
}
.bar {
background-image: linear-gradient(90deg,#1A3A5C,#C8962E);
height: 10px;
}
@media (max-width: 768px) {
.kpi-grid {
grid-template-columns: repeat(2, 1fr);
}
}
body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; color: #1A3A5C; padding: 24px; }
</style>
</head>
<body>
<div class="header">
<h1>🏗️ 危大方案编审进度看板</h1>
<p>2026 年开工项目 · 中东区域公司 · 数据日期 2026-06-08 · GLM 美化版</p>
</div>
<!-- KPI -->
<div class="kpi-grid">
<div class="kpi"><div class="label">方案总数</div><div class="num">52</div></div>
<div class="kpi"><div class="label">已审批</div><div class="num" style="color:#2E7D32">29</div></div>
<div class="kpi"><div class="label">审批率</div><div class="num" style="color:#C8962E">56%</div></div>
<div class="kpi"><div class="label">超规方案</div><div class="num" style="color:#D94E34">22</div></div>
<div class="kpi"><div class="label">未完成</div><div class="num">23</div></div>
<div class="kpi"><div class="label">橙色预警</div><div class="num" style="color:#C8962E">1</div></div>
<div class="kpi"><div class="label">红色预警</div><div class="num" style="color:#D94E34">0</div></div>
<div class="kpi"><div class="label">覆盖项目</div><div class="num">12</div></div>
</div>
<!-- 预警明细 -->
<div class="table-wrap">
<h2>⚠️ 预警方案明细</h2>
<table>
<tr><th>项目名称</th><th>方案名称</th><th>国别</th><th>计划开工</th><th>预警等级</th></tr>
<tr><td>阿联酋迪拜马克图姆国际机场地下结构工程项目</td><td>BHS处理中心/GSE隧道现浇板专项施工方案(4包...</td><td>阿拉伯联合酋长国</td><td>2026-06-10 00:00:00</td><td><span class="badge-alert">orange</span></td></tr>
</table>
</div>
<!-- 项目明细 -->
<div class="table-wrap">
<h2>📋 项目明细(按方案总数排序)</h2>
<table>
<tr><th>项目名称</th><th class="num">方案</th><th class="num">已审批</th><th class="num">超规</th><th>进度</th><th class="num">审批率</th><th>国别</th></tr>
<tr><td>阿联酋迪拜马克图姆国际机场地下结构工程项目</td><td class="num">35</td><td class="num">20</td><td class="num">13</td><td class="num"><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:57%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-warn">57%</span></td><td>阿拉伯联合酋长国</td></tr>
<tr><td>阿联酋沙迦卡尔巴摩托艇港开发项目</td><td class="num">3</td><td class="num">0</td><td class="num">1</td><td class="num"><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:4%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-alert">0%</span></td><td>阿拉伯联合酋长国</td></tr>
<tr><td>阿联酋阿布扎比汽车基地房建项目</td><td class="num">3</td><td class="num">0</td><td class="num">2</td><td class="num"><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:4%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-alert">0%</span></td><td>阿拉伯联合酋长国</td></tr>
<tr><td>沙特吉赞基础下游工业城5至7号路间矿业区基础设施一期项目</td><td class="num">2</td><td class="num">2</td><td class="num">0</td><td class="num"><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:100%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-ok">100%</span></td><td>沙特阿拉伯</td></tr>
<tr><td>阿联酋阿布扎比汽车基地基础设施项目</td><td class="num">2</td><td class="num">0</td><td class="num">2</td><td class="num"><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:4%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-alert">0%</span></td><td>阿拉伯联合酋长国</td></tr>
<tr><td>沙特利雅得德拉伊耶门二期多功能场馆及办公楼房建项目</td><td class="num">1</td><td class="num">1</td><td class="num">1</td><td class="num"><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:100%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-ok">100%</span></td><td>沙特阿拉伯</td></tr>
<tr><td>沙特利雅得南二环路第三标段项目</td><td class="num">1</td><td class="num">1</td><td class="num">0</td><td class="num"><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:100%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-ok">100%</span></td><td>沙特阿拉伯</td></tr>
<tr><td>卡塔尔道路运营及维护框架项目</td><td class="num">1</td><td class="num">1</td><td class="num">1</td><td class="num"><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:100%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-ok">100%</span></td><td>卡塔尔</td></tr>
<tr><td>沙特吉赞基础下游工业城3区1巷独栋别墅一期项目</td><td class="num">1</td><td class="num">1</td><td class="num">1</td><td class="num"><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:100%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-ok">100%</span></td><td>沙特阿拉伯</td></tr>
<tr><td>沙特达曼港第一和第二集装箱码头升级改造工程</td><td class="num">1</td><td class="num">1</td><td class="num">1</td><td class="num"><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:100%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-ok">100%</span></td><td>沙特阿拉伯</td></tr>
<tr><td>阿联酋阿布扎比哈里发港EGA泊位翻新工程项目</td><td class="num">1</td><td class="num">1</td><td class="num">0</td><td class="num"><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:100%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-ok">100%</span></td><td>阿拉伯联合酋长国</td></tr>
<tr><td>阿联酋阿布扎比哈里发工业园B区食品基地工程</td><td class="num">1</td><td class="num">1</td><td class="num">0</td><td class="num"><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:100%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-ok">100%</span></td><td>阿拉伯联合酋长国</td></tr>
<tr style="font-weight:bold;background:#1A3A5C;color:#fff">
<td>合计</td><td class="num">52</td><td class="num">29</td><td class="num">22</td>
<td><div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:56%;height:10px;border-radius:5px"></div></div></td><td class="num"><span class="badge-warn">56%</span></td><td></td>
</tr>
</table>
</div>
<!-- 国别汇总 -->
<div class="table-wrap">
<h2>🌍 按国别汇总</h2>
<table>
<tr><th>国别</th><th class="num">项目数</th><th class="num">方案总数</th><th class="num">已审批</th><th class="num">超规</th><th class="num">审批率</th></tr>
<tr><td>卡塔尔</td><td class="num">1</td><td class="num">1</td><td class="num">1</td><td class="num">1</td><td class="num"><span class="badge-ok">100%</span></td></tr>
<tr><td>沙特阿拉伯</td><td class="num">5</td><td class="num">6</td><td class="num">6</td><td class="num">3</td><td class="num"><span class="badge-ok">100%</span></td></tr>
<tr><td>阿拉伯联合酋长国</td><td class="num">6</td><td class="num">45</td><td class="num">22</td><td class="num">18</td><td class="num"><span class="badge-alert">49%</span></td></tr>
</table>
</div>
<div style="text-align:center;color:#8B7355;padding:20px;font-size:12px">
<p>中国港湾中东区域公司 技术部 · GLM-4-Flash 美化版 · 自动生成于 2026-06-08</p>
</div>
</body></html>

View File

@ -0,0 +1,240 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>中东区域公司 技术部月报台账看板 · GLM 美化版 | 2026-06-08</title>
<style>
body {
background: #FAF6EE;
font-family: 'Microsoft YaHei', 'PingFang SC';
}
.header {
text-align: center;
border-bottom: 3px solid #C8962E;
}
.header h1 {
font-size: 32px;
color: #1A3A5C;
}
.header p {
font-size: 14px;
color: #8B7355;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(26,58,92,.08);
}
.card h2 {
font-size: 18px;
color: #1A3A5C;
border-bottom: 1px solid #EFE9D9;
}
.metric {
display: flex;
padding: 8px 0;
border-bottom: 1px solid #EFE9D9;
}
.metric .label {
font-size: 14px;
color: #5C7A99;
}
.metric .value {
font-size: 18px;
font-weight: bold;
color: #1A3A5C;
}
.value.good {
color: #2E7D32;
}
.value.warn {
color: #C8962E;
}
.value.bad {
color: #D94E34;
}
.table-wrap {
background: white;
border-radius: 12px;
padding: 24px;
}
.table-wrap h2 {
font-size: 18px;
color: #1A3A5C;
border-bottom: 2px solid #C8962E;
}
th {
background: #1A3A5C;
color: white;
font-size: 14px;
}
td {
padding: 8px;
border-bottom: 1px solid #EFE9D9;
}
tr:nth-child(even) td {
background: #F5F1E8;
}
tr:hover td {
background: #FFF8E1;
}
.bar {
height: 10px;
background: linear-gradient(90deg, #1A3A5C, #C8962E);
}
.bar-bg {
height: 10px;
background: #EFE9D9;
}
.red {
color: #D94E34;
}
.orange {
color: #C8962E;
}
.green {
color: #2E7D32;
}
.num {
text-align: right;
tabular-nums;
}
.footer {
text-align: center;
font-size: 12px;
color: #8B7355;
}
@media (max-width: 900px) {
.grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="header">
<h1>🏗️ 中东区域公司 · 技术部月报台账看板</h1>
<p>数据日期: 2026-06-08 | 生成时间: 2026-06-09 | GLM-4-Flash 美化版</p>
</div>
<div class="grid">
<div class="card">
<h2>📋 危大方案编审进度</h2>
<div class="metric"><span class="label">有效方案总数</span><span class="value">135</span></div>
<div class="metric"><span class="label">超一定规模</span><span class="value warn">52</span></div>
<div class="metric"><span class="label">已审批</span><span class="value good">112</span></div>
<div class="metric"><span class="label">审批率</span><span class="value warn">83%</span></div>
<div class="metric"><span class="label">预警信号</span><span class="value warn">5</span></div>
<div class="metric"><span class="label">覆盖项目</span><span class="value">38</span></div>
</div>
<div class="card">
<h2>🔄 项目启动动态跟踪</h2>
<div class="metric"><span class="label">跟踪项目数</span><span class="value">80</span></div>
<div class="metric"><span class="label">总任务数</span><span class="value">5290</span></div>
<div class="metric"><span class="label">已完成</span><span class="value good">1443</span></div>
<div class="metric"><span class="label">逾期</span><span class="value bad">803</span></div>
<div class="metric"><span class="label">完成率</span><span class="value">27%</span></div>
</div>
<div class="card">
<h2>💰 营业额与产值</h2>
<div class="metric"><span class="label">中东项目数</span><span class="value">97</span></div>
<div class="metric"><span class="label">活跃在建</span><span class="value">47</span></div>
<div class="metric"><span class="label">签约合同额</span><span class="value">$95万</span></div>
<div class="metric"><span class="label">本年营业收入</span><span class="value good">$5万</span></div>
<div class="metric"><span class="label">累计营业收入</span><span class="value">$26万</span></div>
</div>
</div>
<div class="table-wrap">
<h2>📋 危大方案编审 — 项目排行 Top 15</h2>
<table>
<tr><th>项目名称</th><th class="num">方案</th><th class="num">已审批</th><th class="num">超规</th><th>审批进度</th><th class="num">审批率</th><th>国别</th></tr>
<tr><td>阿联酋迪拜马克图姆国际机场地下结构工程项目</td><td class="num">35</td><td class="num">20</td><td class="num">13</td><td><div class="bar-bg"><div class="bar" style="width:57%"></div></div></td><td class="num">57%</td><td>阿拉伯联合酋长国</td></tr>
<tr><td>沙特红海Laheq岛连接路和跨海桥工程</td><td class="num">11</td><td class="num">11</td><td class="num">7</td><td><div class="bar-bg"><div class="bar" style="width:100%"></div></div></td><td class="num">100%</td><td>沙特阿拉伯</td></tr>
<tr><td>沙特利雅得南二环路第三标段项目</td><td class="num">10</td><td class="num">10</td><td class="num">2</td><td><div class="bar-bg"><div class="bar" style="width:100%"></div></div></td><td class="num">100%</td><td>沙特阿拉伯</td></tr>
<tr><td>阿联酋阿布扎比中岛公园一期1C道桥项目</td><td class="num">7</td><td class="num">7</td><td class="num">3</td><td><div class="bar-bg"><div class="bar" style="width:100%"></div></div></td><td class="num">100%</td><td>阿拉伯联合酋长国</td></tr>
<tr><td>沙特吉达市中心综合开发基础设施项目</td><td class="num">7</td><td class="num">7</td><td class="num">3</td><td><div class="bar-bg"><div class="bar" style="width:100%"></div></div></td><td class="num">100%</td><td>沙特阿拉伯</td></tr>
<tr><td>沙特吉赞基础下游工业城保税区至人工岛连接桥项目</td><td class="num">6</td><td class="num">6</td><td class="num">3</td><td><div class="bar-bg"><div class="bar" style="width:100%"></div></div></td><td class="num">100%</td><td>沙特阿拉伯</td></tr>
<tr><td>阿联酋哈伊马角永利岛连接桥</td><td class="num">6</td><td class="num">6</td><td class="num">1</td><td><div class="bar-bg"><div class="bar" style="width:100%"></div></div></td><td class="num">100%</td><td>阿拉伯联合酋长国</td></tr>
<tr><td>阿联酋阿布扎比哈里发工业园B区食品基地工程</td><td class="num">4</td><td class="num">4</td><td class="num">0</td><td><div class="bar-bg"><div class="bar" style="width:100%"></div></div></td><td class="num">100%</td><td>阿拉伯联合酋长国</td></tr>
<tr><td>沙特达曼港第一和第二集装箱码头升级改造工程</td><td class="num">3</td><td class="num">3</td><td class="num">2</td><td><div class="bar-bg"><div class="bar" style="width:100%"></div></div></td><td class="num">100%</td><td>沙特阿拉伯</td></tr>
<tr><td>沙特红海舒莱亚码头及潜水中心项目</td><td class="num">3</td><td class="num">3</td><td class="num">0</td><td><div class="bar-bg"><div class="bar" style="width:100%"></div></div></td><td class="num">100%</td><td>沙特阿拉伯</td></tr>
<tr><td>沙特吉赞基础下游工业城3区1巷独栋别墅一期项目</td><td class="num">3</td><td class="num">3</td><td class="num">2</td><td><div class="bar-bg"><div class="bar" style="width:100%"></div></div></td><td class="num">100%</td><td>沙特阿拉伯</td></tr>
<tr><td>沙特利雅得德拉伊耶门二期多功能场馆及办公楼房建项目</td><td class="num">3</td><td class="num">3</td><td class="num">1</td><td><div class="bar-bg"><div class="bar" style="width:100%"></div></div></td><td class="num">100%</td><td>沙特阿拉伯</td></tr>
<tr><td>阿联酋沙迦卡尔巴摩托艇港开发项目</td><td class="num">3</td><td class="num">0</td><td class="num">1</td><td><div class="bar-bg"><div class="bar" style="width:4%"></div></div></td><td class="num">0%</td><td>阿拉伯联合酋长国</td></tr>
<tr><td>阿联酋阿布扎比汽车基地房建项目</td><td class="num">3</td><td class="num">0</td><td class="num">2</td><td><div class="bar-bg"><div class="bar" style="width:4%"></div></div></td><td class="num">0%</td><td>阿拉伯联合酋长国</td></tr>
<tr><td>阿联酋阿布扎比瑞姆岛7号和8号跨海桥工程</td><td class="num">2</td><td class="num">2</td><td class="num">2</td><td><div class="bar-bg"><div class="bar" style="width:100%"></div></div></td><td class="num">100%</td><td>阿拉伯联合酋长国</td></tr>
</table>
</div>
<div class="table-wrap">
<h2>🔄 项目启动跟踪 — 逾期项目 Top 15</h2>
<table>
<tr><th>项目名称</th><th class="num">总任务</th><th class="num">已完成</th><th class="num">逾期</th><th class="num">完成率</th></tr>
<tr><td>阿联酋阿布扎比汽车基地房建项目</td><td class="num">64</td><td class="num">1</td><td class="num orange">39</td><td class="num">2%</td></tr>
<tr><td>沙特苏盖克电厂缆桩修复工程项目</td><td class="num">66</td><td class="num">21</td><td class="num orange">32</td><td class="num">32%</td></tr>
<tr><td>卡塔尔道路运营及维护框架项目</td><td class="num">66</td><td class="num">6</td><td class="num orange">30</td><td class="num">9%</td></tr>
<tr><td>阿联酋高铁迪拜段土建项目</td><td class="num">66</td><td class="num">10</td><td class="num orange">29</td><td class="num">15%</td></tr>
<tr><td>沙特利雅得南二环路第三标段项目</td><td class="num">65</td><td class="num">5</td><td class="num orange">28</td><td class="num">8%</td></tr>
<tr><td>沙特红海拉赫克岛疏浚吹填工程</td><td class="num">67</td><td class="num">22</td><td class="num orange">22</td><td class="num">33%</td></tr>
<tr><td>阿联酋沙迦卡尔巴摩托艇港开发项目</td><td class="num">66</td><td class="num">5</td><td class="num orange">22</td><td class="num">8%</td></tr>
<tr><td>阿联酋阿布扎比西部三岛水工项目</td><td class="num">66</td><td class="num">8</td><td class="num orange">22</td><td class="num">12%</td></tr>
<tr><td>沙特海尔港钢厂前期工作土方工程</td><td class="num">67</td><td class="num">20</td><td class="num green">20</td><td class="num">30%</td></tr>
<tr><td>沙特吉达市中心综合开发基础设施项目</td><td class="num">67</td><td class="num">16</td><td class="num green">20</td><td class="num">24%</td></tr>
<tr><td>阿联酋阿布扎比马斯努阿岛水工项目</td><td class="num">64</td><td class="num">16</td><td class="num green">20</td><td class="num">25%</td></tr>
<tr><td>阿联酋富查伊拉军港扩建附属设施包</td><td class="num">65</td><td class="num">9</td><td class="num green">20</td><td class="num">14%</td></tr>
<tr><td>阿联酋阿布扎比沙哈马港升级改造项目</td><td class="num">64</td><td class="num">13</td><td class="num green">19</td><td class="num">20%</td></tr>
<tr><td>沙特穆卡布四方城土方包</td><td class="num">67</td><td class="num">21</td><td class="num green">18</td><td class="num">31%</td></tr>
<tr><td>沙特红海Laheq岛连接路和跨海桥工程</td><td class="num">67</td><td class="num">21</td><td class="num green">17</td><td class="num">31%</td></tr>
</table>
</div>
<div class="table-wrap">
<h2>💰 营收项目 Top 10(本年营收排序)</h2>
<table>
<tr><th>项目名称</th><th class="num">签约合同额(万美元)</th><th class="num">本年营收</th><th class="num">累计营收</th></tr>
<tr><td colspan="4" style="text-align:center;color:#8B7355">暂无营收数据</td></tr>
</table>
</div>
<div class="footer">
<p>中国港湾中东区域公司 技术部 · GLM-4-Flash 美化版 · 数据源: OA 项管平台</p>
</div>
</body></html>

View File

@ -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'<style[^>]*>([\s\S]+?)</style>', 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'<tr><td>{row.get("项目名称","")}</td><td class="num">{total}</td><td class="num">{done}</td>'
f'<td class="num">{over}</td>'
f'<td><div class="bar-bg"><div class="bar" style="width:{bar_w}%"></div></div></td>'
f'<td class="num">{rate}%</td><td>{row.get("国别","")}</td></tr>'
)
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'<tr><td>{row.get("项目名称","")}</td><td class="num">{total}</td><td class="num">{done}</td>'
f'<td class="num {cls}">{overdue}</td><td class="num">{rate}%</td></tr>'
)
tracking_html = '\n'.join(tracking_rows[:15]) if tracking_rows else '<tr><td colspan="5" style="text-align:center;color:#8B7355">无逾期数据</td></tr>'
# 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'<tr><td>{name}</td><td class="num">${contract/10000:,.0f}</td>'
f'<td class="num green">${annual/10000:,.0f}</td>'
f'<td class="num">${cumulative/10000:,.0f}</td></tr>'
)
revenue_html = '\n'.join(revenue_rows) if revenue_rows else '<tr><td colspan="4" style="text-align:center;color:#8B7355">暂无营收数据</td></tr>'
approval_pct = m.get('审批率', 'N/A')
approval_cls = 'good' if str(approval_pct).isdigit() and int(approval_pct) >= 80 else 'warn'
html = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>中东区域公司 技术部月报台账看板 · GLM 美化版 | {DATA_DATE}</title>
<style>
{css_content}
</style>
</head>
<body>
<div class="header">
<h1>🏗 中东区域公司 · 技术部月报台账看板</h1>
<p>数据日期: {DATA_DATE} | 生成时间: {TODAY} | GLM-4-Flash 美化版</p>
</div>
<div class="grid">
<div class="card">
<h2>📋 危大方案编审进度</h2>
<div class="metric"><span class="label">有效方案总数</span><span class="value">{m.get('有效方案','N/A')}</span></div>
<div class="metric"><span class="label">超一定规模</span><span class="value warn">{m.get('超规','N/A')}</span></div>
<div class="metric"><span class="label">已审批</span><span class="value good">{m.get('已审批','N/A')}</span></div>
<div class="metric"><span class="label">审批率</span><span class="value {approval_cls}">{approval_pct}</span></div>
<div class="metric"><span class="label">预警信号</span><span class="value {'warn' if m.get('预警',0)>0 else 'good'}">{m.get('预警',0)}</span></div>
<div class="metric"><span class="label">覆盖项目</span><span class="value">{m.get('项目','N/A')}</span></div>
</div>
<div class="card">
<h2>🔄 项目启动动态跟踪</h2>
<div class="metric"><span class="label">跟踪项目数</span><span class="value">{t.get('me_projects','N/A')}</span></div>
<div class="metric"><span class="label">总任务数</span><span class="value">{t.get('total_tasks','N/A')}</span></div>
<div class="metric"><span class="label">已完成</span><span class="value good">{t.get('completed','N/A')}</span></div>
<div class="metric"><span class="label">逾期</span><span class="value {'bad' if t.get('overdue',0)>500 else 'warn'}">{t.get('overdue','N/A')}</span></div>
<div class="metric"><span class="label">完成率</span><span class="value">{fmt_pct(t.get('completed',0), t.get('total_tasks',1))}</span></div>
</div>
<div class="card">
<h2>💰 营业额与产值</h2>
<div class="metric"><span class="label">中东项目数</span><span class="value">{r.get('all_me','N/A')}</span></div>
<div class="metric"><span class="label">活跃在建</span><span class="value">{r.get('active','N/A')}</span></div>
<div class="metric"><span class="label">签约合同额</span><span class="value">{fmt_money(r.get('total_contract',0))}</span></div>
<div class="metric"><span class="label">本年营业收入</span><span class="value good">{fmt_money(r.get('annual_revenue',0))}</span></div>
<div class="metric"><span class="label">累计营业收入</span><span class="value">{fmt_money(r.get('cumulative_revenue',0))}</span></div>
</div>
</div>
<div class="table-wrap">
<h2>📋 危大方案编审 项目排行 Top 15</h2>
<table>
<tr><th>项目名称</th><th class="num">方案</th><th class="num">已审批</th><th class="num">超规</th><th>审批进度</th><th class="num">审批率</th><th>国别</th></tr>
{methods_html}
</table>
</div>
<div class="table-wrap">
<h2>🔄 项目启动跟踪 逾期项目 Top 15</h2>
<table>
<tr><th>项目名称</th><th class="num">总任务</th><th class="num">已完成</th><th class="num">逾期</th><th class="num">完成率</th></tr>
{tracking_html}
</table>
</div>
<div class="table-wrap">
<h2>💰 营收项目 Top 10(本年营收排序)</h2>
<table>
<tr><th>项目名称</th><th class="num">签约合同额(万美元)</th><th class="num">本年营收</th><th class="num">累计营收</th></tr>
{revenue_html}
</table>
</div>
<div class="footer">
<p>中国港湾中东区域公司 技术部 · GLM-4-Flash 美化版 · 数据源: OA 项管平台</p>
</div>
</body></html>'''
out = BASE / 'dashboard_glm.html'
out.write_text(html, encoding='utf-8')
print(f'{out}')
print(f' {len(html):,} bytes')

View File

@ -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); } }
输出要求
只输出 <style> 标签内的 CSS 代码, 不写 HTML body, 不要解释, 不要 markdown 围栏
开头不要写 ```css 之类的标记, 直接写 CSSCSS 控制在 60 行以内"""
css_content = call_glm(css_prompt, max_tokens=1500)
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: 24px; }
.kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin: 24px 0; }
.kpi { background: linear-gradient(135deg, #fff 0%, #F5F1E8 100%); border-radius: 12px; padding: 24px; box-shadow: 0 4px 16px rgba(26,58,92,.08); border-top: 4px solid #C8962E; text-align: center; }
.kpi .num { font-size: 48px; font-weight: 700; color: #1A3A5C; margin: 8px 0; }
.kpi .label { color: #5C7A99; font-size: 14px; }
.table-wrap { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,.06); }
.table-wrap h2 { color: #1A3A5C; font-size: 22px; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 2px solid #C8962E; }
table { width: 100%; border-collapse: collapse; }
th { background: #1A3A5C; color: #fff; padding: 12px 10px; text-align: left; font-size: 14px; }
td { padding: 10px; border-bottom: 1px solid #EFE9D9; }
tr:nth-child(even) td { background: #F5F1E8; }
tr:hover td { background: #FFF8E1; }
.badge-ok { background: #2E7D32; color: #fff; padding: 3px 10px; border-radius: 12px; font-size: 12px; }
.badge-warn { background: #C8962E; color: #fff; padding: 3px 10px; border-radius: 12px; font-size: 12px; }
.badge-alert { background: #D94E34; color: #fff; padding: 3px 10px; border-radius: 12px; font-size: 12px; }
.bar { height: 10px; border-radius: 5px; background: linear-gradient(90deg, #1A3A5C, #C8962E); }
.num { text-align: right; font-variant-numeric: tabular-nums; }
.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; }
@media (max-width: 768px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } }
'''
print(' 使用 fallback 主题')
else:
# 清洗 GLM 输出, 抽取 <style>...</style> 或 ```css...``` 块
import re
m = re.search(r'<style[^>]*>([\s\S]+?)</style>', 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'<span class="{cls}">{rate}%</span>'
def rate_bar(rate, total_n):
width = max(int(rate), 4) if total_n else 0
return f'<div class="bar-bg" style="background:#EFE9D9;border-radius:5px;width:100px;display:inline-block;height:10px"><div class="bar" style="width:{width}%;height:10px;border-radius:5px"></div></div>'
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'<tr><td>{name}</td><td class="num">{t}</td><td class="num">{d}</td>'
f'<td class="num">{s}</td><td class="num">{rate_bar(rate, t)}</td>'
f'<td class="num">{rate_badge(rate)}</td><td>{r.get("国别","")}</td></tr>'
)
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'<tr><td>{name}</td><td class="num">{proj_n}</td><td class="num">{t}</td>'
f'<td class="num">{d}</td><td class="num">{s}</td><td class="num">{rate_badge(c)}</td></tr>'
)
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'<tr><td>{r["项目名称"]}</td><td>{r["方案名称"][:50]}...</td>'
f'<td>{r["所属国别"]}</td><td>{r["分部分项工程计划开工日期"]}</td>'
f'<td><span class="badge-alert">{r["预警信号"]}</span></td></tr>'
)
warning_html = '\n'.join(warning_rows) if warning_rows else '<tr><td colspan="5" style="text-align:center;color:#8B7355">🟢 当前无预警方案</td></tr>'
html = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>危大方案编审进度看板 · GLM 美化版 | 中东区域公司</title>
<style>
{css_content}
</style>
</head>
<body>
<div class="header">
<h1>🏗 危大方案编审进度看板</h1>
<p>2026 年开工项目 · 中东区域公司 · 数据日期 {DATA_DATE} · GLM 美化版</p>
</div>
<!-- KPI -->
<div class="kpi-grid">
<div class="kpi"><div class="label">方案总数</div><div class="num">{total}</div></div>
<div class="kpi"><div class="label">已审批</div><div class="num" style="color:#2E7D32">{completed}</div></div>
<div class="kpi"><div class="label">审批率</div><div class="num" style="color:#C8962E">{approved_pct}%</div></div>
<div class="kpi"><div class="label">超规方案</div><div class="num" style="color:#D94E34">{oversized}</div></div>
<div class="kpi"><div class="label">未完成</div><div class="num">{unfinished_n}</div></div>
<div class="kpi"><div class="label">橙色预警</div><div class="num" style="color:#C8962E">{orange_n}</div></div>
<div class="kpi"><div class="label">红色预警</div><div class="num" style="color:#D94E34">{red_n}</div></div>
<div class="kpi"><div class="label">覆盖项目</div><div class="num">{len(ps)}</div></div>
</div>
<!-- 预警明细 -->
<div class="table-wrap">
<h2> 预警方案明细</h2>
<table>
<tr><th>项目名称</th><th>方案名称</th><th>国别</th><th>计划开工</th><th>预警等级</th></tr>
{warning_html}
</table>
</div>
<!-- 项目明细 -->
<div class="table-wrap">
<h2>📋 项目明细按方案总数排序</h2>
<table>
<tr><th>项目名称</th><th class="num">方案</th><th class="num">已审批</th><th class="num">超规</th><th>进度</th><th class="num">审批率</th><th>国别</th></tr>
{projects_html}
<tr style="font-weight:bold;background:#1A3A5C;color:#fff">
<td>合计</td><td class="num">{total}</td><td class="num">{completed}</td><td class="num">{oversized}</td>
<td>{rate_bar(approved_pct, total)}</td><td class="num">{rate_badge(approved_pct)}</td><td></td>
</tr>
</table>
</div>
<!-- 国别汇总 -->
<div class="table-wrap">
<h2>🌍 按国别汇总</h2>
<table>
<tr><th>国别</th><th class="num">项目数</th><th class="num">方案总数</th><th class="num">已审批</th><th class="num">超规</th><th class="num">审批率</th></tr>
{country_html}
</table>
</div>
<div style="text-align:center;color:#8B7355;padding:20px;font-size:12px">
<p>中国港湾中东区域公司 技术部 · GLM-4-Flash 美化版 · 自动生成于 {DATA_DATE}</p>
</div>
</body></html>'''
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')

335
src/gen_pptx_glm.py Normal file
View File

@ -0,0 +1,335 @@
#!/usr/bin/env python3
"""
GLM 美化版 · 危大方案编审进度看板 PPTX · v2
差异 vs 原版 gen_pptx.py:
- 浅米黄底 (#FAF6EE) 替代暗色
- 蓝金渐变标题条替代纯色蓝条
- 卡片圆角+阴影, 加大留白
- KPI 字号 4452
- 国别图改为横向进度条
- 预警颜色用约定橙/ (#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')