2026-06-11 16:06:34 +08:00
|
|
|
|
"""
|
|
|
|
|
|
高考志愿填报可视化报告生成器 V2.0
|
|
|
|
|
|
支持:雷达图、热力图、对比表、风险检测
|
|
|
|
|
|
新增:HTML导出、PDF导出、精美样式
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试导入可选依赖
|
|
|
|
|
|
try:
|
|
|
|
|
|
from jinja2 import Template
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
HAS_JINJA = True
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
HAS_JINJA = False
|
|
|
|
|
|
print("警告: jinja2 未安装,HTML模板功能将受限")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-06-14 13:02:03 +08:00
|
|
|
|
from weasyprint import HTML # type: ignore[import-untyped]
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
HAS_WEASYPRINT = True
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
HAS_WEASYPRINT = False
|
|
|
|
|
|
print("警告: weasyprint 未安装,PDF导出功能不可用")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_student_radar(student_profile):
|
|
|
|
|
|
"""
|
|
|
|
|
|
生成考生画像雷达图数据
|
|
|
|
|
|
"""
|
|
|
|
|
|
scores = {
|
|
|
|
|
|
"兴趣匹配度": student_profile.get("interest_match", 0),
|
|
|
|
|
|
"能力匹配度": student_profile.get("ability_match", 0),
|
|
|
|
|
|
"就业匹配度": student_profile.get("employment_match", 0),
|
|
|
|
|
|
"家庭适配度": student_profile.get("family_match", 0),
|
|
|
|
|
|
}
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 综合得分计算
|
|
|
|
|
|
weighted_score = (
|
2026-06-13 14:49:58 +08:00
|
|
|
|
scores["兴趣匹配度"] * 0.3
|
|
|
|
|
|
+ scores["能力匹配度"] * 0.3
|
|
|
|
|
|
+ scores["就业匹配度"] * 0.25
|
|
|
|
|
|
+ scores["家庭适配度"] * 0.15
|
2026-06-11 16:06:34 +08:00
|
|
|
|
)
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 生成ASCII雷达图
|
|
|
|
|
|
radar_chart = f"""
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────────┐
|
2026-06-13 14:49:58 +08:00
|
|
|
|
│ 🎯 {student_profile.get("name", "考生")} 画像雷达图 │
|
2026-06-11 16:06:34 +08:00
|
|
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ 兴趣匹配度 │
|
2026-06-13 14:49:58 +08:00
|
|
|
|
│ {scores["兴趣匹配度"]:>3}/10 │
|
2026-06-11 16:06:34 +08:00
|
|
|
|
│ │ │
|
2026-06-13 14:49:58 +08:00
|
|
|
|
│ {min(scores["能力匹配度"], 10):>2}/10 ─────────┼───────── {min(scores["就业匹配度"], 10):>2}/10 │
|
2026-06-11 16:06:34 +08:00
|
|
|
|
│ 能力匹配度 │ 就业匹配度 │
|
|
|
|
|
|
│ │ │
|
|
|
|
|
|
│ 【{weighted_score:.1f}/10】 │
|
|
|
|
|
|
│ 综合得分 │
|
|
|
|
|
|
│ │ │
|
2026-06-13 14:49:58 +08:00
|
|
|
|
│ {scores["家庭适配度"]:>3}/10 │
|
2026-06-11 16:06:34 +08:00
|
|
|
|
│ 家庭适配度 │
|
|
|
|
|
|
│ │
|
|
|
|
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ 评分说明: │
|
|
|
|
|
|
│ • 兴趣匹配:基于霍兰德职业兴趣测试结果 │
|
|
|
|
|
|
│ • 能力匹配:学科成绩与专业要求的匹配度 │
|
|
|
|
|
|
│ • 就业匹配:专业就业前景评估 │
|
|
|
|
|
|
│ • 家庭适配:经济条件、地域要求的符合度 │
|
|
|
|
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
"""
|
|
|
|
|
|
return radar_chart, weighted_score, scores
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_school_comparison(volunteer_list):
|
|
|
|
|
|
"""
|
|
|
|
|
|
生成院校对比决策表
|
|
|
|
|
|
"""
|
|
|
|
|
|
table = """
|
|
|
|
|
|
┌──────────────────────────────────────────────────────────────────────┐
|
|
|
|
|
|
│ 🎯 志愿方案可视化对比 │
|
|
|
|
|
|
├────────────┬────────────┬──────────┬─────────┬─────────┬───────────┤
|
|
|
|
|
|
│ 志愿类型 │ 院校 │ 专业 │ 录取概率│ 匹配指数│ 推荐指数 │
|
|
|
|
|
|
├────────────┼────────────┼──────────┼─────────┼─────────┼───────────┤
|
|
|
|
|
|
"""
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
for idx, vol in enumerate(volunteer_list, 1):
|
2026-06-13 14:49:58 +08:00
|
|
|
|
v_type = vol.get("type", "稳")
|
|
|
|
|
|
emoji = {"冲": "🔴", "稳": "🟡", "保": "🟢"}.get(v_type, "⚪")
|
|
|
|
|
|
|
|
|
|
|
|
prob_bar = "█" * int(vol.get("probability", 0) / 10) + "░" * (
|
|
|
|
|
|
10 - int(vol.get("probability", 0) / 10)
|
|
|
|
|
|
)
|
|
|
|
|
|
match_score = vol.get("match_score", 0)
|
|
|
|
|
|
stars = "⭐" * int(match_score / 20) + "☆" * (5 - int(match_score / 20))
|
|
|
|
|
|
|
|
|
|
|
|
table += f"""│ {emoji} {v_type:>2} {idx:>2} │ {vol.get("school", "待定"):<10} │ {vol.get("major", "待定"):<8} │ {prob_bar} │ {match_score:>3} │ {stars} │
|
2026-06-11 16:06:34 +08:00
|
|
|
|
├────────────┼────────────┼──────────┼─────────┼─────────┼───────────┤
|
|
|
|
|
|
"""
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
table += """│ │
|
|
|
|
|
|
│ 图例: │
|
|
|
|
|
|
│ 录取概率 █████ 90%+ ████░ 80%+ ███░░ 60%+ ██░░░ 40%+ █░░░░ <20%│
|
|
|
|
|
|
│ 匹配指数 90-100 完美 80-90 高度 70-80 中度 <70 需谨慎 │
|
|
|
|
|
|
└──────────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
"""
|
|
|
|
|
|
return table
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_major_heatmap(majors):
|
|
|
|
|
|
"""
|
|
|
|
|
|
生成专业匹配度热力图
|
|
|
|
|
|
"""
|
|
|
|
|
|
heatmap = """
|
|
|
|
|
|
┌──────────────────────────────────────────────────────────────────────┐
|
|
|
|
|
|
│ 🔥 兴趣-专业匹配度热力图 │
|
|
|
|
|
|
├──────────────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ │
|
|
|
|
|
|
"""
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
for major in majors:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
name = major.get("name", "")
|
|
|
|
|
|
score = major.get("match_score", 0)
|
|
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 生成热力条
|
|
|
|
|
|
if score >= 90:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
bar = "█" * 20
|
|
|
|
|
|
status = "强烈推荐"
|
2026-06-11 16:06:34 +08:00
|
|
|
|
elif score >= 80:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
bar = "█" * 17 + "░" * 3
|
|
|
|
|
|
status = "推荐选择"
|
2026-06-11 16:06:34 +08:00
|
|
|
|
elif score >= 70:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
bar = "█" * 14 + "░" * 6
|
|
|
|
|
|
status = "可以考虑"
|
2026-06-11 16:06:34 +08:00
|
|
|
|
elif score >= 60:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
bar = "█" * 11 + "░" * 9
|
|
|
|
|
|
status = "谨慎考虑"
|
2026-06-11 16:06:34 +08:00
|
|
|
|
elif score >= 40:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
bar = "█" * 8 + "░" * 12
|
|
|
|
|
|
status = "不太建议"
|
2026-06-11 16:06:34 +08:00
|
|
|
|
else:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
bar = "█" * 5 + "░" * 15
|
|
|
|
|
|
status = "不推荐"
|
|
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
heatmap += f"│ {name:<12} {bar} {score:>3}% {status:<12} │\n"
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
heatmap += """│ │
|
|
|
|
|
|
│ 热力指数:████ 90-100% ████ 80-90% ███░ 70-80% ██░░ 60-70% │
|
|
|
|
|
|
│ █░░░ 30-60% ░░░░ <30% (不推荐) │
|
|
|
|
|
|
└──────────────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
"""
|
|
|
|
|
|
return heatmap
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def detect_risks(student_profile, volunteer_list):
|
|
|
|
|
|
"""
|
|
|
|
|
|
智能风险检测
|
|
|
|
|
|
"""
|
|
|
|
|
|
risks = []
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 检查位次差距
|
|
|
|
|
|
for vol in volunteer_list:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
if vol.get("type") == "冲":
|
|
|
|
|
|
if vol.get("probability", 0) < 30:
|
2026-06-11 16:06:34 +08:00
|
|
|
|
risks.append({
|
2026-06-13 14:49:58 +08:00
|
|
|
|
"level": "warning",
|
|
|
|
|
|
"item": f"{vol.get('school')}录取概率过低",
|
|
|
|
|
|
"desc": f"录取概率仅{vol.get('probability')}%,需要增加相近备选",
|
2026-06-11 16:06:34 +08:00
|
|
|
|
})
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 检查学科匹配
|
2026-06-13 14:49:58 +08:00
|
|
|
|
weak_subjects = student_profile.get("weak_subjects", [])
|
2026-06-11 16:06:34 +08:00
|
|
|
|
for vol in volunteer_list:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
required = vol.get("required_subjects", [])
|
2026-06-11 16:06:34 +08:00
|
|
|
|
for subj in required:
|
|
|
|
|
|
if subj in weak_subjects:
|
|
|
|
|
|
risks.append({
|
2026-06-13 14:49:58 +08:00
|
|
|
|
"level": "danger",
|
|
|
|
|
|
"item": f"{vol.get('school')}-{vol.get('major')}学科不匹配",
|
|
|
|
|
|
"desc": f"该专业需要{subj},但考生{subj}为弱项",
|
2026-06-11 16:06:34 +08:00
|
|
|
|
})
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 检查梯度合理性
|
2026-06-13 14:49:58 +08:00
|
|
|
|
types_count = {"冲": 0, "稳": 0, "保": 0}
|
2026-06-11 16:06:34 +08:00
|
|
|
|
for vol in volunteer_list:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
v_type = vol.get("type", "稳")
|
2026-06-11 16:06:34 +08:00
|
|
|
|
types_count[v_type] = types_count.get(v_type, 0) + 1
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
total = len(volunteer_list)
|
|
|
|
|
|
if total > 0:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
if types_count["保"] / total < 0.2:
|
2026-06-11 16:06:34 +08:00
|
|
|
|
risks.append({
|
2026-06-13 14:49:58 +08:00
|
|
|
|
"level": "danger",
|
|
|
|
|
|
"item": "保底志愿不足",
|
|
|
|
|
|
"desc": f"保底志愿仅占{types_count['保'] / total * 100:.0f}%,建议至少30%",
|
2026-06-11 16:06:34 +08:00
|
|
|
|
})
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 生成风险报告
|
|
|
|
|
|
risk_report = """
|
|
|
|
|
|
┌──────────────────────────────────────────────────────────────────────┐
|
|
|
|
|
|
│ 🚦 志愿填报风险检测报告 │
|
|
|
|
|
|
├──────────────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ │
|
|
|
|
|
|
"""
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
|
|
|
|
|
danger_list = [r for r in risks if r["level"] == "danger"]
|
|
|
|
|
|
warning_list = [r for r in risks if r["level"] == "warning"]
|
|
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
if danger_list:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
risk_report += (
|
|
|
|
|
|
"│ 🔴 高风险项目(必须修改): │\n"
|
|
|
|
|
|
)
|
2026-06-11 16:06:34 +08:00
|
|
|
|
for risk in danger_list:
|
|
|
|
|
|
risk_report += f"│ ✗ {risk['item']:<30} │\n"
|
|
|
|
|
|
risk_report += f"│ → {risk['desc']:<50} │\n"
|
|
|
|
|
|
risk_report += "│ │\n"
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
if warning_list:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
risk_report += (
|
|
|
|
|
|
"│ 🟡 中风险项目(建议调整): │\n"
|
|
|
|
|
|
)
|
2026-06-11 16:06:34 +08:00
|
|
|
|
for risk in warning_list:
|
|
|
|
|
|
risk_report += f"│ ⚠ {risk['item']:<30} │\n"
|
|
|
|
|
|
risk_report += f"│ → {risk['desc']:<50} │\n"
|
|
|
|
|
|
risk_report += "│ │\n"
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
if not risks:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
risk_report += (
|
|
|
|
|
|
"│ 🟢 恭喜!未检测到高风险项目,当前方案可以安全填报 │\n"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
risk_report += """│ │
|
|
|
|
|
|
└──────────────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
"""
|
|
|
|
|
|
return risk_report, risks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_html_report(student_data, volunteer_list, output_file=None):
|
|
|
|
|
|
"""
|
|
|
|
|
|
生成HTML可视化报告
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not HAS_JINJA:
|
|
|
|
|
|
print("错误: 需要安装 jinja2 才能生成HTML报告")
|
|
|
|
|
|
print("运行: pip3 install --user jinja2")
|
|
|
|
|
|
return None
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 计算雷达图数据
|
|
|
|
|
|
radar_data = {
|
2026-06-13 14:49:58 +08:00
|
|
|
|
"interest": student_data.get("interest_match", 0),
|
|
|
|
|
|
"ability": student_data.get("ability_match", 0),
|
|
|
|
|
|
"employment": student_data.get("employment_match", 0),
|
|
|
|
|
|
"family": student_data.get("family_match", 0),
|
2026-06-11 16:06:34 +08:00
|
|
|
|
}
|
2026-06-13 14:49:58 +08:00
|
|
|
|
radar_data["average"] = sum(radar_data.values()) / 4
|
|
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# HTML模板
|
2026-06-13 14:49:58 +08:00
|
|
|
|
html_template = """<!DOCTYPE html>
|
2026-06-11 16:06:34 +08:00
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
<title>高考志愿填报方案 - {{ name }}</title>
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC", sans-serif;
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
.container {
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.header {
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 40px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
|
|
|
|
|
|
.header .subtitle { font-size: 1.2em; opacity: 0.9; }
|
|
|
|
|
|
.info-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
padding: 30px;
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-card {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-card .label { color: #666; font-size: 0.9em; margin-bottom: 5px; }
|
|
|
|
|
|
.info-card .value { font-size: 1.5em; font-weight: bold; color: #667eea; }
|
|
|
|
|
|
.section {
|
|
|
|
|
|
padding: 40px;
|
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
|
}
|
|
|
|
|
|
.section:last-child { border-bottom: none; }
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
font-size: 1.8em;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chart-container {
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
padding: 30px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.school-table {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.school-table th, .school-table td {
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
|
}
|
|
|
|
|
|
.school-table th {
|
|
|
|
|
|
background: #667eea;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
.school-table tr:hover { background: #f8f9fa; }
|
|
|
|
|
|
.badge {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 5px 15px;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
font-size: 0.85em;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
.badge-red { background: #ffebee; color: #c62828; }
|
|
|
|
|
|
.badge-yellow { background: #fff3e0; color: #ef6c00; }
|
|
|
|
|
|
.badge-green { background: #e8f5e9; color: #2e7d32; }
|
|
|
|
|
|
.heatmap { display: flex; flex-direction: column; gap: 10px; }
|
|
|
|
|
|
.heat-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 15px;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
.heat-bar {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
background: linear-gradient(90deg, #ff6b6b 0%, #ffd93d 50%, #6bcf7f 100%);
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
.heat-marker {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: -5px;
|
|
|
|
|
|
width: 30px;
|
|
|
|
|
|
height: 30px;
|
|
|
|
|
|
background: #667eea;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
box-shadow: 0 2px 10px rgba(102,126,234,0.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
.risk-list { list-style: none; }
|
|
|
|
|
|
.risk-item {
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.risk-danger { background: #ffebee; border-left: 4px solid #c62828; }
|
|
|
|
|
|
.risk-warning { background: #fff3e0; border-left: 4px solid #ef6c00; }
|
|
|
|
|
|
.risk-safe { background: #e8f5e9; border-left: 4px solid #2e7d32; }
|
|
|
|
|
|
.footer {
|
|
|
|
|
|
background: #333;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 30px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
@media print {
|
|
|
|
|
|
body { background: white; }
|
|
|
|
|
|
.container { box-shadow: none; }
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<h1>🎓 高考志愿填报方案</h1>
|
|
|
|
|
|
<p class="subtitle">{{ name }} | {{ province }} | {{ score }}分 | 位次: {{ rank }}</p>
|
|
|
|
|
|
<p style="margin-top: 10px; opacity: 0.8;">生成时间: {{ generated_at }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="info-grid">
|
|
|
|
|
|
<div class="info-card">
|
|
|
|
|
|
<div class="label">考生姓名</div>
|
|
|
|
|
|
<div class="value">{{ name }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-card">
|
|
|
|
|
|
<div class="label">高考总分</div>
|
|
|
|
|
|
<div class="value">{{ score }}分</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-card">
|
|
|
|
|
|
<div class="label">全省位次</div>
|
|
|
|
|
|
<div class="value">{{ rank }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-card">
|
|
|
|
|
|
<div class="label">综合匹配</div>
|
|
|
|
|
|
<div class="value">{{ radar.average }}/10</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<h2 class="section-title">📊 考生画像分析</h2>
|
|
|
|
|
|
<div class="chart-container">
|
|
|
|
|
|
<canvas id="radarChart" width="400" height="300"></canvas>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<h2 class="section-title">🎯 志愿方案对比</h2>
|
|
|
|
|
|
<table class="school-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>类型</th>
|
|
|
|
|
|
<th>院校</th>
|
|
|
|
|
|
<th>专业</th>
|
|
|
|
|
|
<th>录取概率</th>
|
|
|
|
|
|
<th>匹配度</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{% for vol in volunteers %}
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
{% if vol.type == '冲' %}
|
|
|
|
|
|
<span class="badge badge-red">🔴 冲</span>
|
|
|
|
|
|
{% elif vol.type == '稳' %}
|
|
|
|
|
|
<span class="badge badge-yellow">🟡 稳</span>
|
|
|
|
|
|
{% else %}
|
|
|
|
|
|
<span class="badge badge-green">🟢 保</span>
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td>{{ vol.school }}</td>
|
|
|
|
|
|
<td>{{ vol.major }}</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<div style="background: #eee; border-radius: 10px; overflow: hidden;">
|
|
|
|
|
|
<div style="background: linear-gradient(90deg, #ff6b6b, #ffd93d, #6bcf7f);
|
|
|
|
|
|
height: 20px; width: {{ vol.probability }}%;
|
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
|
color: white; font-size: 0.8em; font-weight: bold;">
|
|
|
|
|
|
{{ vol.probability }}%
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td>{{ vol.match_score }}分</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
{% endfor %}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<h2 class="section-title">🔥 专业匹配度热力图</h2>
|
|
|
|
|
|
<div class="heatmap">
|
|
|
|
|
|
{% for major in majors %}
|
|
|
|
|
|
<div class="heat-item">
|
|
|
|
|
|
<span style="width: 120px; font-weight: bold;">{{ major.name }}</span>
|
|
|
|
|
|
<div class="heat-bar">
|
|
|
|
|
|
<div class="heat-marker" style="left: {{ major.match_score }}%;"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span style="width: 60px; text-align: right; font-weight: bold;">{{ major.match_score }}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{% endfor %}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<h2 class="section-title">🚦 风险检测报告</h2>
|
|
|
|
|
|
{% if risks %}
|
|
|
|
|
|
<ul class="risk-list">
|
|
|
|
|
|
{% for risk in risks %}
|
|
|
|
|
|
<li class="risk-item {% if risk.level == 'danger' %}risk-danger{% elif risk.level == 'warning' %}risk-warning{% else %}risk-safe{% endif %}">
|
|
|
|
|
|
{% if risk.level == 'danger' %}✗{% elif risk.level == 'warning' %}⚠{% else %}✓{% endif %}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>{{ risk.item }}</strong>
|
|
|
|
|
|
<p style="color: #666; margin-top: 5px;">{{ risk.desc }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
{% endfor %}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
{% else %}
|
|
|
|
|
|
<div class="risk-item risk-safe">
|
|
|
|
|
|
✓ <strong>恭喜!当前方案未检测到高风险,可以安全填报</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="footer">
|
|
|
|
|
|
<p>© 2026 高考志愿填报智能系统</p>
|
|
|
|
|
|
<p style="margin-top: 10px; opacity: 0.7;">本报告仅供参考,请以官方信息为准</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
// 雷达图
|
|
|
|
|
|
const ctx = document.getElementById('radarChart').getContext('2d');
|
|
|
|
|
|
new Chart(ctx, {
|
|
|
|
|
|
type: 'radar',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: ['兴趣匹配', '能力匹配', '就业匹配', '家庭适配'],
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: '匹配度',
|
|
|
|
|
|
data: [{{ radar.interest }}, {{ radar.ability }}, {{ radar.employment }}, {{ radar.family }}],
|
|
|
|
|
|
backgroundColor: 'rgba(102, 126, 234, 0.2)',
|
|
|
|
|
|
borderColor: 'rgba(102, 126, 234, 1)',
|
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
|
pointBackgroundColor: 'rgba(102, 126, 234, 1)',
|
|
|
|
|
|
pointSize: 6
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
r: {
|
|
|
|
|
|
beginAtZero: true,
|
|
|
|
|
|
max: 100,
|
|
|
|
|
|
ticks: {
|
|
|
|
|
|
stepSize: 20
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
legend: { display: false }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
2026-06-13 14:49:58 +08:00
|
|
|
|
</html>"""
|
|
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
template = Template(html_template)
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 准备数据
|
2026-06-13 14:49:58 +08:00
|
|
|
|
majors = [
|
|
|
|
|
|
{"name": vol["major"], "match_score": vol["match_score"]}
|
|
|
|
|
|
for vol in volunteer_list
|
|
|
|
|
|
]
|
2026-06-11 16:06:34 +08:00
|
|
|
|
_, risks = detect_risks(student_data, volunteer_list)
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
html_content = template.render(
|
2026-06-13 14:49:58 +08:00
|
|
|
|
name=student_data.get("name", "考生"),
|
|
|
|
|
|
province=student_data.get("province", "未知"),
|
|
|
|
|
|
score=student_data.get("score", 0),
|
|
|
|
|
|
rank=student_data.get("rank", 0),
|
2026-06-11 16:06:34 +08:00
|
|
|
|
radar=radar_data,
|
|
|
|
|
|
volunteers=volunteer_list,
|
|
|
|
|
|
majors=majors,
|
|
|
|
|
|
risks=risks,
|
2026-06-13 14:49:58 +08:00
|
|
|
|
generated_at=datetime.now().strftime("%Y年%m月%d日 %H:%M"),
|
2026-06-11 16:06:34 +08:00
|
|
|
|
)
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 保存HTML
|
|
|
|
|
|
if output_file is None:
|
|
|
|
|
|
output_file = f"/tmp/gaokao_report_{student_data.get('name', 'unknown')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
|
|
|
|
|
with open(output_file, "w", encoding="utf-8") as f:
|
2026-06-11 16:06:34 +08:00
|
|
|
|
f.write(html_content)
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
print(f"✓ HTML报告已生成: {output_file}")
|
|
|
|
|
|
return output_file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_pdf_from_html(html_file, pdf_file=None):
|
|
|
|
|
|
"""
|
|
|
|
|
|
将HTML转换为PDF
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not HAS_WEASYPRINT:
|
|
|
|
|
|
print("错误: 需要安装 weasyprint 才能生成PDF")
|
|
|
|
|
|
print("运行: pip3 install --user weasyprint")
|
|
|
|
|
|
return None
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
if pdf_file is None:
|
2026-06-13 14:49:58 +08:00
|
|
|
|
pdf_file = html_file.replace(".html", ".pdf")
|
|
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
try:
|
|
|
|
|
|
HTML(filename=html_file).write_pdf(pdf_file)
|
|
|
|
|
|
print(f"✓ PDF报告已生成: {pdf_file}")
|
|
|
|
|
|
return pdf_file
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"✗ PDF生成失败: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-13 14:49:58 +08:00
|
|
|
|
def generate_visual_report(student_data, volunteer_list, output_format="all"):
|
2026-06-11 16:06:34 +08:00
|
|
|
|
"""
|
|
|
|
|
|
生成完整可视化报告(支持多种格式)
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
参数:
|
|
|
|
|
|
student_data: 考生数据
|
|
|
|
|
|
volunteer_list: 志愿列表
|
|
|
|
|
|
output_format: 输出格式 ('md', 'html', 'pdf', 'all')
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
返回:
|
|
|
|
|
|
生成的文件路径列表
|
|
|
|
|
|
"""
|
|
|
|
|
|
results = []
|
2026-06-13 14:49:58 +08:00
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
2026-06-11 16:06:34 +08:00
|
|
|
|
base_name = f"gaokao_report_{student_data.get('name', 'unknown')}_{timestamp}"
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 1. 生成Markdown报告
|
2026-06-13 14:49:58 +08:00
|
|
|
|
if output_format in ["md", "all"]:
|
2026-06-11 16:06:34 +08:00
|
|
|
|
md_content = f"""# 高考志愿填报方案报告
|
|
|
|
|
|
|
2026-06-13 14:49:58 +08:00
|
|
|
|
**生成时间**: {datetime.now().strftime("%Y年%m月%d日 %H:%M")}
|
|
|
|
|
|
**考生姓名**: {student_data.get("name", "考生")}
|
|
|
|
|
|
**考生省份**: {student_data.get("province", "未知")}
|
|
|
|
|
|
**高考总分**: {student_data.get("score", 0)}分
|
|
|
|
|
|
**全省位次**: {student_data.get("rank", 0)}名
|
2026-06-11 16:06:34 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 一、考生画像雷达图
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
radar_chart, weighted_score, scores = generate_student_radar(student_data)
|
|
|
|
|
|
md_content += radar_chart
|
|
|
|
|
|
md_content += f"\n**综合匹配指数**: {weighted_score:.1f}/10\n\n"
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
md_content += "## 二、志愿方案可视化对比\n\n"
|
|
|
|
|
|
md_content += generate_school_comparison(volunteer_list)
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
md_content += "## 三、专业匹配度热力图\n\n"
|
2026-06-13 14:49:58 +08:00
|
|
|
|
majors = [
|
|
|
|
|
|
{"name": vol["major"], "match_score": vol["match_score"]}
|
|
|
|
|
|
for vol in volunteer_list
|
|
|
|
|
|
]
|
2026-06-11 16:06:34 +08:00
|
|
|
|
md_content += generate_major_heatmap(majors)
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
md_content += "## 四、风险检测报告\n\n"
|
|
|
|
|
|
risk_report, _ = detect_risks(student_data, volunteer_list)
|
|
|
|
|
|
md_content += risk_report
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
md_file = f"/tmp/{base_name}.md"
|
2026-06-13 14:49:58 +08:00
|
|
|
|
with open(md_file, "w", encoding="utf-8") as f:
|
2026-06-11 16:06:34 +08:00
|
|
|
|
f.write(md_content)
|
|
|
|
|
|
print(f"✓ Markdown报告已生成: {md_file}")
|
|
|
|
|
|
results.append(md_file)
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 2. 生成HTML报告
|
|
|
|
|
|
html_file = None
|
2026-06-13 14:49:58 +08:00
|
|
|
|
if output_format in ["html", "pdf", "all"]:
|
2026-06-11 16:06:34 +08:00
|
|
|
|
html_file = f"/tmp/{base_name}.html"
|
|
|
|
|
|
html_file = generate_html_report(student_data, volunteer_list, html_file)
|
|
|
|
|
|
if html_file:
|
|
|
|
|
|
results.append(html_file)
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 3. 生成PDF报告
|
2026-06-13 14:49:58 +08:00
|
|
|
|
if output_format in ["pdf", "all"] and html_file:
|
2026-06-11 16:06:34 +08:00
|
|
|
|
pdf_file = f"/tmp/{base_name}.pdf"
|
|
|
|
|
|
pdf_file = generate_pdf_from_html(html_file, pdf_file)
|
|
|
|
|
|
if pdf_file:
|
|
|
|
|
|
results.append(pdf_file)
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 示例使用
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
# 示例考生数据
|
|
|
|
|
|
student = {
|
2026-06-13 14:49:58 +08:00
|
|
|
|
"name": "李明",
|
|
|
|
|
|
"province": "浙江省",
|
|
|
|
|
|
"score": 612,
|
|
|
|
|
|
"rank": 15230,
|
|
|
|
|
|
"interest_match": 85,
|
|
|
|
|
|
"ability_match": 90,
|
|
|
|
|
|
"employment_match": 88,
|
|
|
|
|
|
"family_match": 95,
|
|
|
|
|
|
"weak_subjects": ["化学", "语文"],
|
2026-06-11 16:06:34 +08:00
|
|
|
|
}
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 示例志愿列表
|
|
|
|
|
|
volunteers = [
|
|
|
|
|
|
{
|
2026-06-13 14:49:58 +08:00
|
|
|
|
"school": "浙江大学",
|
|
|
|
|
|
"major": "计算机类",
|
|
|
|
|
|
"type": "冲",
|
|
|
|
|
|
"probability": 35,
|
|
|
|
|
|
"match_score": 95,
|
|
|
|
|
|
"required_subjects": ["数学", "物理"],
|
2026-06-11 16:06:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-06-13 14:49:58 +08:00
|
|
|
|
"school": "杭州电子科技大学",
|
|
|
|
|
|
"major": "计算机类",
|
|
|
|
|
|
"type": "稳",
|
|
|
|
|
|
"probability": 70,
|
|
|
|
|
|
"match_score": 92,
|
|
|
|
|
|
"required_subjects": ["数学", "物理"],
|
2026-06-11 16:06:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-06-13 14:49:58 +08:00
|
|
|
|
"school": "浙江工业大学",
|
|
|
|
|
|
"major": "软件工程",
|
|
|
|
|
|
"type": "稳",
|
|
|
|
|
|
"probability": 80,
|
|
|
|
|
|
"match_score": 88,
|
|
|
|
|
|
"required_subjects": ["数学"],
|
2026-06-11 16:06:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-06-13 14:49:58 +08:00
|
|
|
|
"school": "浙江理工大学",
|
|
|
|
|
|
"major": "软件工程",
|
|
|
|
|
|
"type": "保",
|
|
|
|
|
|
"probability": 95,
|
|
|
|
|
|
"match_score": 82,
|
|
|
|
|
|
"required_subjects": ["数学"],
|
|
|
|
|
|
},
|
2026-06-11 16:06:34 +08:00
|
|
|
|
]
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
print("高考志愿填报可视化报告生成器 V2.0")
|
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
print()
|
2026-06-13 14:49:58 +08:00
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
# 生成所有格式
|
2026-06-13 14:49:58 +08:00
|
|
|
|
files = generate_visual_report(student, volunteers, output_format="all")
|
|
|
|
|
|
|
2026-06-11 16:06:34 +08:00
|
|
|
|
print()
|
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
print("生成完成!文件列表:")
|
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
for f in files:
|
|
|
|
|
|
print(f" • {f}")
|
|
|
|
|
|
print()
|
|
|
|
|
|
print("提示:")
|
|
|
|
|
|
print(" • HTML文件可直接在浏览器中打开")
|
|
|
|
|
|
print(" • PDF文件可直接打印或分享")
|
|
|
|
|
|
print(" • Markdown文件可编辑和转换")
|