feat: 审批流程模块 (T审批A审批)
- 新增 backend/app/api/approval.py 审批API - 前端H5支持发起审批、审批操作 - 添加审批卡片弹窗组件 - 路由注册审批模块
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
# =============================================================================
|
||||
# 🎁 惊喜 1: 项目健康度仪表盘
|
||||
# =============================================================================
|
||||
# 用途: 一键生成项目健康度总览 HTML(明早桌面打开即用)
|
||||
# 跑法: python scripts/dashboard.py
|
||||
# 产物: docs/dashboard.html
|
||||
# =============================================================================
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
OUTPUT = PROJECT_ROOT / "docs" / "dashboard.html"
|
||||
|
||||
|
||||
def count_lines(glob_pattern: str) -> int:
|
||||
"""统计符合 glob 的代码总行数"""
|
||||
import glob
|
||||
total = 0
|
||||
for f in glob.glob(glob_pattern, recursive=True):
|
||||
if os.path.isfile(f):
|
||||
try:
|
||||
with open(f, "r", encoding="utf-8", errors="ignore") as fp:
|
||||
total += sum(1 for _ in fp)
|
||||
except Exception:
|
||||
pass
|
||||
return total
|
||||
|
||||
|
||||
def count_files(glob_pattern: str) -> int:
|
||||
import glob
|
||||
return sum(1 for f in glob.glob(glob_pattern, recursive=True) if os.path.isfile(f))
|
||||
|
||||
|
||||
def git_info() -> dict:
|
||||
"""拿 git 仓库信息"""
|
||||
try:
|
||||
result = {
|
||||
"branch": subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
cwd=PROJECT_ROOT, capture_output=True, text=True
|
||||
).stdout.strip(),
|
||||
"last_commit": subprocess.run(
|
||||
["git", "log", "-1", "--format=%h %s"],
|
||||
cwd=PROJECT_ROOT, capture_output=True, text=True
|
||||
).stdout.strip(),
|
||||
"commit_count": subprocess.run(
|
||||
["git", "rev-list", "--count", "HEAD"],
|
||||
cwd=PROJECT_ROOT, capture_output=True, text=True
|
||||
).stdout.strip(),
|
||||
}
|
||||
return result
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def main():
|
||||
# 1. 代码统计
|
||||
stats = {
|
||||
"backend_python_files": count_files("backend/app/**/*.py"),
|
||||
"backend_python_lines": count_lines("backend/app/**/*.py"),
|
||||
"frontend_admin_files": count_files("frontend-admin/src/**/*.{vue,ts,js}"),
|
||||
"frontend_agent_files": count_files("frontend-agent/src/**/*.{vue,ts,js}"),
|
||||
"frontend_h5_files": count_files("frontend-h5/src/**/*.{vue,ts,js}"),
|
||||
"frontend_portal_files": count_files("frontend-portal/src/**/*.{vue,ts,js}"),
|
||||
"docs_files": count_files("docs/**/*.md"),
|
||||
"scripts_files": count_files("scripts/**/*.sh"),
|
||||
"tests_files": count_files("backend/tests/**/*.py"),
|
||||
}
|
||||
|
||||
# 2. 文档统计
|
||||
docs_path = PROJECT_ROOT / "docs"
|
||||
doc_categories = {
|
||||
"评审报告": len(list((docs_path / "评审报告").glob("*.md"))) if (docs_path / "评审报告").exists() else 0,
|
||||
"审计报告": len(list((docs_path / "审计报告").glob("*.md"))) if (docs_path / "审计报告").exists() else 0,
|
||||
"ADRs": len(list((docs_path / "ADRs").glob("*.md"))) if (docs_path / "ADRs").exists() else 0,
|
||||
"SOPs": len(list((docs_path / "SOPs").glob("*.md"))) if (docs_path / "SOPs").exists() else 0,
|
||||
"路线图": len(list((docs_path / "路线图").glob("*.md"))) if (docs_path / "路线图").exists() else 0,
|
||||
}
|
||||
|
||||
# 3. 风险统计(解析风险跟踪表)
|
||||
risk_file = docs_path / "风险跟踪表.md"
|
||||
risk_stats = {"P0_remaining": 0, "P1": 0, "P2": 0, "P3": 0, "M": 0, "L": 0}
|
||||
if risk_file.exists():
|
||||
text = risk_file.read_text(encoding="utf-8")
|
||||
for level in ["P0", "P1", "P2", "P3", "M", "L"]:
|
||||
risk_stats[f"{level}_total"] = text.count(f"### {level}-")
|
||||
risk_stats[f"{level}_remaining"] = text.count(f"### {level}-") - text.count("✅")
|
||||
|
||||
# 4. Git 信息
|
||||
g = git_info()
|
||||
|
||||
# 5. 模板渲染
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>企微 IT 智能服务台 - 健康度仪表盘</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}}
|
||||
.container {{ max-width: 1400px; margin: 0 auto; }}
|
||||
h1 {{ color: white; margin-bottom: 20px; text-align: center; font-size: 2.2em; }}
|
||||
.timestamp {{ color: rgba(255,255,255,0.8); text-align: center; margin-bottom: 30px; }}
|
||||
.grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; }}
|
||||
.card {{
|
||||
background: white; border-radius: 12px; padding: 24px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||
transition: transform 0.2s;
|
||||
}}
|
||||
.card:hover {{ transform: translateY(-2px); }}
|
||||
.card h2 {{ font-size: 1.1em; color: #555; margin-bottom: 12px; }}
|
||||
.big-number {{ font-size: 2.4em; font-weight: bold; color: #667eea; }}
|
||||
.label {{ color: #888; font-size: 0.9em; }}
|
||||
.stat-row {{
|
||||
display: flex; justify-content: space-between;
|
||||
padding: 6px 0; border-bottom: 1px solid #f0f0f0;
|
||||
}}
|
||||
.stat-row:last-child {{ border: none; }}
|
||||
.badge {{
|
||||
display: inline-block; padding: 4px 10px;
|
||||
border-radius: 20px; font-size: 0.85em; margin: 2px;
|
||||
}}
|
||||
.badge.green {{ background: #d4edda; color: #155724; }}
|
||||
.badge.yellow {{ background: #fff3cd; color: #856404; }}
|
||||
.badge.red {{ background: #f8d7da; color: #721c24; }}
|
||||
.badge.blue {{ background: #d1ecf1; color: #0c5460; }}
|
||||
.git-info {{
|
||||
background: #282c34; color: #abb2bf;
|
||||
padding: 16px; border-radius: 8px; font-family: 'Consolas', monospace;
|
||||
font-size: 0.9em; line-height: 1.6;
|
||||
}}
|
||||
.git-info .hash {{ color: #61afef; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚀 企微 IT 智能服务台 - 健康度仪表盘</h1>
|
||||
<div class="timestamp">生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- 概览 -->
|
||||
<div class="card">
|
||||
<h2>📊 代码规模</h2>
|
||||
<div class="big-number">{stats['backend_python_lines']:,}</div>
|
||||
<div class="label">后端 Python 代码行</div>
|
||||
<div style="margin-top: 12px;">
|
||||
<div class="stat-row"><span>后端 Python 文件</span><strong>{stats['backend_python_files']}</strong></div>
|
||||
<div class="stat-row"><span>Admin 前端</span><strong>{stats['frontend_admin_files']} 文件</strong></div>
|
||||
<div class="stat-row"><span>Agent 前端</span><strong>{stats['frontend_agent_files']} 文件</strong></div>
|
||||
<div class="stat-row"><span>H5 前端</span><strong>{stats['frontend_h5_files']} 文件</strong></div>
|
||||
<div class="stat-row"><span>Portal 前端</span><strong>{stats['frontend_portal_files']} 文件</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文档统计 -->
|
||||
<div class="card">
|
||||
<h2>📚 文档</h2>
|
||||
<div class="big-number">{stats['docs_files']}</div>
|
||||
<div class="label">文档总数</div>
|
||||
<div style="margin-top: 12px;">
|
||||
{''.join(f'<div class="stat-row"><span>{k}</span><strong>{v}</strong></div>' for k, v in doc_categories.items())}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 风险状态 -->
|
||||
<div class="card">
|
||||
<h2>🛡️ 风险状态</h2>
|
||||
<div class="big-number" style="color: #dc3545;">{risk_stats.get('P0_remaining', 0)}</div>
|
||||
<div class="label">P0 遗留(需立即修)</div>
|
||||
<div style="margin-top: 12px;">
|
||||
<div class="stat-row"><span>P1 中危</span><span class="badge yellow">{risk_stats.get('P1_remaining', 0)} 待修</span></div>
|
||||
<div class="stat-row"><span>P2 低危</span><span class="badge yellow">{risk_stats.get('P2_remaining', 0)} 待修</span></div>
|
||||
<div class="stat-row"><span>M 中</span><span class="badge blue">{risk_stats.get('M_remaining', 0)} 待修</span></div>
|
||||
<div class="stat-row"><span>L 低</span><span class="badge blue">{risk_stats.get('L_remaining', 0)} 待修</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 脚本与测试 -->
|
||||
<div class="card">
|
||||
<h2>🛠️ 工具链</h2>
|
||||
<div class="big-number">{stats['scripts_files']}</div>
|
||||
<div class="label">自动化脚本</div>
|
||||
<div style="margin-top: 12px;">
|
||||
<div class="stat-row"><span>后端测试</span><strong>{stats['tests_files']} 文件</strong></div>
|
||||
<div class="stat-row"><span>安全审计</span><span class="badge green">✅ 已配</span></div>
|
||||
<div class="stat-row"><span>API 文档</span><span class="badge green">✅ 已配</span></div>
|
||||
<div class="stat-row"><span>备份脚本</span><span class="badge green">✅ 已配</span></div>
|
||||
<div class="stat-row"><span>Pre-commit</span><span class="badge green">✅ 已配</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Git 状态 -->
|
||||
<div class="card" style="grid-column: span 2;">
|
||||
<h2>📦 Git 状态</h2>
|
||||
<div class="git-info">
|
||||
<div>分支: <span class="hash">{g.get('branch', '?')}</span></div>
|
||||
<div>提交数: <span class="hash">{g.get('commit_count', '?')}</span></div>
|
||||
<div>最近提交: <span class="hash">{g.get('last_commit', '?')}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模块完成度 -->
|
||||
<div class="card" style="grid-column: span 3;">
|
||||
<h2>✅ 阶段完成度</h2>
|
||||
<div style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-top: 12px;">
|
||||
<div style="text-align: center;">
|
||||
<div class="big-number" style="font-size: 1.8em; color: #28a745;">66%</div>
|
||||
<div class="label">阶段 1</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div class="big-number" style="font-size: 1.8em; color: #ffc107;">0%</div>
|
||||
<div class="label">阶段 2(转人工)</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div class="big-number" style="font-size: 1.8em; color: #6c757d;">0%</div>
|
||||
<div class="label">阶段 3(H5+WS)</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div class="big-number" style="font-size: 1.8em; color: #6c757d;">规划中</div>
|
||||
<div class="label">阶段 4(AI Wingman)</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div class="big-number" style="font-size: 1.8em; color: #6c757d;">规划中</div>
|
||||
<div class="label">阶段 5(自动化)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; color: rgba(255,255,255,0.7); margin-top: 40px; font-size: 0.9em;">
|
||||
企微 IT 智能服务台 · 健康度仪表盘 v1.0
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT.write_text(html, encoding="utf-8")
|
||||
print(f"✅ 仪表盘已生成: {OUTPUT}")
|
||||
print(f" 打开方式: 直接在浏览器打开 file:///{OUTPUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user