
Web Design
- 60 installs
- 760 repo stars
- Updated July 15, 2026
- countbot-ai/countbot
Generate single-page HTML sites (reports, landing pages, data viz) with Tailwind, Chart.js, and Font Awesome, and deploy to Cloudflare Pages.
About
Generates polished single-page HTML using Tailwind CSS, Chart.js, and Font Awesome, then deploys one-click to Cloudflare Pages. A developer uses it to produce report pages, landing pages, or data visualizations.
- Tailwind + Chart.js + Font Awesome stack with per-page unique styling
- One-click deploy to Cloudflare Pages; no purple gradients or emoji
Web Design by the numbers
- 60 all-time installs (skills.sh)
- Ranked #1,212 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/countbot-ai/countbot --skill web-designAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 60 |
|---|---|
| repo stars | ★ 760 |
| Last updated | July 15, 2026 |
| Repository | countbot-ai/countbot ↗ |
What it does
Generate single-page HTML sites (reports, landing pages, data viz) with Tailwind, Chart.js, and Font Awesome, and deploy to Cloudflare Pages.
Files
网页设计与部署
生成精美单页 HTML 并部署到 Cloudflare Pages。
设计规范
设计语言
根据用户需求自由设计,每次生成的页面应有独特的视觉风格,避免千篇一律。
设计约束:
- 不使用紫色渐变配色
- 不使用 Emoji 表情符号,统一使用 Font Awesome 图标代替
- 布局:灵活运用 Tailwind CSS 的 Grid / Flex 布局,根据内容量和类型选择合适的排版
- 卡片组件:圆角、阴影、悬停动效等可根据风格调整
- 图标:使用 Font Awesome 6.x
<i class="fa-solid fa-xxx"> - 图表:使用 Chart.js,支持 bar / line / doughnut / radar / polarArea 等
- 响应式:确保移动端和桌面端都有良好的展示效果
推荐在线资源(CDN)
<!-- 核心 -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- 动画 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
<script src="https://unpkg.com/aos@2.3.4/dist/aos.js"></script>
<link rel="stylesheet" href="https://unpkg.com/aos@2.3.4/dist/aos.css">
<!-- 字体 -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap">
<!-- 图表增强 -->
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
<!-- 轮播 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<!-- 代码高亮(技术类页面) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- 数字滚动动画 -->
<script src="https://cdn.jsdelivr.net/npm/countup.js@2/dist/countUp.umd.min.js"></script>
<!-- 打字机效果 -->
<script src="https://cdn.jsdelivr.net/npm/typed.js@2.1.0/dist/typed.umd.js"></script>
<!-- 粒子背景 -->
<script src="https://cdn.jsdelivr.net/npm/tsparticles-slim@2/tsparticles.slim.bundle.min.js"></script>
<!-- Markdown 渲染(文档类页面) -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- 图片灯箱 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lightgallery@2/css/lightgallery-bundle.min.css">
<script src="https://cdn.jsdelivr.net/npm/lightgallery@2/lightgallery.umd.min.js"></script>按需引入,不必全部加载。根据页面类型选择合适的资源。
按需参考 assets/template.html 了解一种可能的实现方式(白底卡片式布局),但不要每次都照搬。
部署到 Cloudflare Pages
重要规则
- 如果用户没有明确指定部署到哪个项目,必须使用八位随机字母数字前缀 + 内容名称作为项目名(如
a3f8k2m1-product-intro、x7b9d4e2-annual-report),避免项目名冲突 - 只有用户明确说"部署到 xxx 项目"或"更新 xxx"时,才部署到已有项目
- 随机前缀使用小写字母和数字混合,每次部署生成新的前缀
配置
编辑 skills/web-design/scripts/config.json:
{
"api_token": "YOUR_CLOUDFLARE_API_TOKEN"
}API Token 获取:Cloudflare Dashboard > Create Token > Cloudflare Pages: Edit
account_id 无需配置,脚本会通过 API 自动获取。
命令行调用
纯 Python + REST API 实现,无需 wrangler 或 Node.js。
# 部署单个 HTML 文件(自动创建项目,自动作为 index.html)
python3 skills/web-design/scripts/deploy.py deploy index.html --project my-site
# 部署整个目录
python3 skills/web-design/scripts/deploy.py deploy ./dist --project my-site
# 部署到预览分支
python3 skills/web-design/scripts/deploy.py deploy ./dist --project my-site --branch preview
# 查看项目列表
python3 skills/web-design/scripts/deploy.py list
# 查看项目部署历史
python3 skills/web-design/scripts/deploy.py deployments --project my-site
部署成功后会返回 https://<project>.pages.dev 的访问地址。
AI 调用场景
用户说"帮我做一个产品介绍页":
1. 根据用户需求设计独特的页面风格 2. 生成 HTML 并保存到本地文件
用户说"部署到线上"(未指定项目):
# 八位随机前缀 + 内容名称,避免冲突
python3 skills/web-design/scripts/deploy.py deploy 文件路径 --project a3f8k2m1-product-intro用户说"更新 xxxx 网站":
python3 skills/web-design/scripts/deploy.py deploy 文件路径 --project prompt123注意事项
- 单文件部署时自动复制为 index.html
- 单文件限制 10MB
- 项目名会作为子域名:
<project>.pages.dev - 首次部署会自动创建项目
- 纯 Python 标准库实现,无需 Node.js 或 wrangler
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>页面标题</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background-color: #FFFFFF;
}
.card {
background-color: white;
border: 1px solid #E5E7EB;
border-radius: 1.25rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
overflow: hidden;
position: relative;
}
.card:hover {
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
.highlight-gradient-blue {
background: linear-gradient(135deg, rgba(59, 130, 246, 0), rgba(59, 130, 246, 0.05));
}
.highlight-gradient-green {
background: linear-gradient(135deg, rgba(16, 185, 129, 0), rgba(16, 185, 129, 0.05));
}
.highlight-gradient-purple {
background: linear-gradient(135deg, rgba(139, 92, 246, 0), rgba(139, 92, 246, 0.05));
}
.highlight-gradient-orange {
background: linear-gradient(135deg, rgba(249, 115, 22, 0), rgba(249, 115, 22, 0.05));
}
.highlight-gradient-red {
background: linear-gradient(135deg, rgba(239, 68, 68, 0), rgba(239, 68, 68, 0.05));
}
.highlight-gradient-teal {
background: linear-gradient(135deg, rgba(20, 184, 166, 0), rgba(20, 184, 166, 0.05));
}
</style>
</head>
<body class="bg-white">
<div class="max-w-7xl mx-auto p-4 md:p-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 md:gap-6">
<!-- 标题卡片:横跨 4 列 -->
<div class="card md:col-span-4 p-8 flex flex-col justify-center items-center text-center">
<div class="absolute top-0 left-0 w-full h-full highlight-gradient-blue"></div>
<h1 class="text-4xl md:text-5xl font-bold text-gray-800 mb-4 z-10">主标题</h1>
<h2 class="text-3xl md:text-4xl font-bold text-blue-600 mb-6 z-10">副标题</h2>
<div class="flex items-center space-x-4 text-gray-500 z-10">
<span><i class="fa-solid fa-tag mr-2"></i>标签一</span>
<span><i class="fa-solid fa-chart-line mr-2"></i>标签二</span>
</div>
</div>
<!-- 大卡片:横跨 2 列 2 行 -->
<div class="card md:col-span-2 md:row-span-2 p-8 flex flex-col justify-between">
<div class="absolute top-0 left-0 w-full h-full highlight-gradient-green"></div>
<div class="z-10">
<h3 class="text-2xl font-bold text-gray-800 mb-4">内容区标题</h3>
<p class="text-gray-600 mb-6">描述文字,介绍核心内容。</p>
<div class="space-y-4">
<div class="flex items-start space-x-3">
<i class="fa-solid fa-server text-green-500 mt-1"></i>
<div>
<h4 class="font-semibold text-gray-700">特性一</h4>
<p class="text-sm text-gray-500">特性描述</p>
</div>
</div>
<div class="flex items-start space-x-3">
<i class="fa-solid fa-puzzle-piece text-green-500 mt-1"></i>
<div>
<h4 class="font-semibold text-gray-700">特性二</h4>
<p class="text-sm text-gray-500">特性描述</p>
</div>
</div>
</div>
</div>
<div class="text-6xl font-bold text-green-500/20 text-right z-10">
<i class="fa-solid fa-layer-group"></i>
</div>
</div>
<!-- 小卡片:1 列 -->
<div class="card p-6 flex flex-col justify-center items-center text-center">
<div class="absolute top-0 left-0 w-full h-full highlight-gradient-purple"></div>
<div class="text-4xl text-purple-500 mb-3 z-10"><i class="fa-solid fa-shield-halved"></i></div>
<h4 class="text-lg font-bold text-gray-800 mb-2 z-10">指标卡片</h4>
<p class="text-sm text-gray-500 z-10">简短说明</p>
</div>
<div class="card p-6 flex flex-col justify-center items-center text-center">
<div class="absolute top-0 left-0 w-full h-full highlight-gradient-purple"></div>
<div class="text-4xl text-purple-500 mb-3 z-10"><i class="fa-solid fa-cubes"></i></div>
<h4 class="text-lg font-bold text-gray-800 mb-2 z-10">指标卡片</h4>
<p class="text-sm text-gray-500 z-10">简短说明</p>
</div>
<!-- 数据对比卡片:横跨 2 列,带数字指标 -->
<div class="card md:col-span-2 p-8">
<div class="absolute top-0 left-0 w-full h-full highlight-gradient-blue"></div>
<div class="flex items-center space-x-4 mb-4 z-10 relative">
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<i class="fa-solid fa-headset text-blue-600 text-xl"></i>
</div>
<div>
<h4 class="text-xl font-bold text-gray-800">场景卡片</h4>
<p class="text-sm text-gray-500">场景描述</p>
</div>
</div>
<p class="text-gray-600 mb-4 z-10 relative">详细说明文字。</p>
<div class="flex items-center space-x-6 z-10 relative">
<div class="text-center">
<div class="text-2xl font-bold text-blue-600">200+</div>
<div class="text-xs text-gray-500">指标一</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-blue-600">85%</div>
<div class="text-xs text-gray-500">指标二</div>
</div>
</div>
</div>
<!-- 图表卡片:横跨 4 列 -->
<div class="card md:col-span-4 p-8">
<div class="absolute top-0 left-0 w-full h-full highlight-gradient-orange"></div>
<h3 class="text-2xl font-bold text-gray-800 mb-6 z-10 relative">图表区域</h3>
<div class="h-80 z-10 relative">
<canvas id="demoChart"></canvas>
</div>
</div>
</div>
</div>
<script>
// 示例图表
const ctx = document.getElementById('demoChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['类别A', '类别B', '类别C', '类别D', '类别E'],
datasets: [{
label: '数据 (%)',
data: [35, 25, 20, 12, 8],
backgroundColor: [
'rgba(16, 185, 129, 0.7)',
'rgba(59, 130, 246, 0.7)',
'rgba(139, 92, 246, 0.7)',
'rgba(249, 115, 22, 0.7)',
'rgba(239, 68, 68, 0.7)'
],
borderColor: [
'rgb(16, 185, 129)',
'rgb(59, 130, 246)',
'rgb(139, 92, 246)',
'rgb(249, 115, 22)',
'rgb(239, 68, 68)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: {
beginAtZero: true,
ticks: { callback: v => v + '%' }
}
}
}
});
</script>
</body>
</html>{
"api_token": ""
}{
"api_token": "YOUR_CLOUDFLARE_API_TOKEN"
}
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Cloudflare Pages 部署工具 - 纯 REST API,只需一个 api_token"""
import argparse
import sys
import json
import os
import hashlib
import mimetypes
import base64
import io
import urllib.request
import urllib.error
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'config.json')
CF_API = "https://api.cloudflare.com/client/v4"
_account_id_cache = None
def load_config():
"""加载配置,只需 api_token 一个字段"""
if not os.path.exists(CONFIG_PATH):
print(f"配置文件不存在: {CONFIG_PATH}", file=sys.stderr)
print(f"请复制 config.json.example 为 config.json 并填写 api_token", file=sys.stderr)
sys.exit(1)
with open(CONFIG_PATH, 'r') as f:
config = json.load(f)
if not config.get('api_token'):
print("配置缺少 api_token", file=sys.stderr)
sys.exit(1)
return config['api_token']
def get_account_id(token):
"""通过 API 自动获取 account_id"""
global _account_id_cache
if _account_id_cache:
return _account_id_cache
result = api_request('GET', '/accounts', token)
if not result.get('success') or not result.get('result'):
print("❌ 无法获取账户信息,请检查 api_token 是否有效", file=sys.stderr)
sys.exit(1)
_account_id_cache = result['result'][0]['id']
return _account_id_cache
def api_request(method, path, token, data=None):
"""发送 Cloudflare API 请求"""
url = f"{CF_API}{path}"
headers = {"Authorization": f"Bearer {token}"}
body = None
if data is not None:
headers["Content-Type"] = "application/json;charset=UTF-8"
body = json.dumps(data).encode('utf-8')
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode('utf-8'))
except urllib.error.HTTPError as e:
err_body = e.read().decode('utf-8')
try:
return json.loads(err_body)
except Exception:
print(f"API 错误: HTTP {e.code} - {err_body}", file=sys.stderr)
sys.exit(1)
def multipart_request(url, token, fields):
"""发送 multipart/form-data 请求"""
boundary = '----CloudflarePagesDeployBoundary'
body_parts = []
for name, value in fields.items():
body_parts.append(f'--{boundary}\r\n'.encode())
body_parts.append(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode())
body_parts.append(value.encode('utf-8') if isinstance(value, str) else value)
body_parts.append(b'\r\n')
body_parts.append(f'--{boundary}--\r\n'.encode())
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": f"multipart/form-data; boundary={boundary}",
}
req = urllib.request.Request(url, data=b''.join(body_parts), headers=headers, method='POST')
try:
with urllib.request.urlopen(req, timeout=120) as resp:
return json.loads(resp.read().decode('utf-8'))
except urllib.error.HTTPError as e:
err_body = e.read().decode('utf-8')
try:
return json.loads(err_body)
except Exception:
print(f"API 错误: HTTP {e.code} - {err_body}", file=sys.stderr)
sys.exit(1)
def collect_files(source_path):
"""收集要部署的文件"""
source_path = os.path.abspath(source_path)
files = {}
if os.path.isfile(source_path):
with open(source_path, 'rb') as f:
content = f.read()
b64 = base64.b64encode(content).decode('ascii')
ct = mimetypes.guess_type(source_path)[0] or 'text/html'
files["/index.html"] = {
"hash": hashlib.md5((b64 + "/index.html").encode()).hexdigest(),
"content_type": ct, "content_b64": b64, "size": len(content),
}
basename = os.path.basename(source_path)
if basename != "index.html":
files[f"/{basename}"] = {
"hash": hashlib.md5((b64 + f"/{basename}").encode()).hexdigest(),
"content_type": ct, "content_b64": b64, "size": len(content),
}
elif os.path.isdir(source_path):
for root, dirs, filenames in os.walk(source_path):
dirs[:] = [d for d in dirs if not d.startswith('.')]
for fname in filenames:
if fname.startswith('.'):
continue
full_path = os.path.join(root, fname)
rel_path = "/" + os.path.relpath(full_path, source_path).replace("\\", "/")
with open(full_path, 'rb') as f:
content = f.read()
b64 = base64.b64encode(content).decode('ascii')
files[rel_path] = {
"hash": hashlib.md5((b64 + rel_path).encode()).hexdigest(),
"content_type": mimetypes.guess_type(full_path)[0] or 'application/octet-stream',
"content_b64": b64, "size": len(content),
}
else:
print(f"路径不存在: {source_path}", file=sys.stderr)
sys.exit(1)
return files
def ensure_project(token, account_id, project_name):
"""确保项目存在,不存在则创建"""
result = api_request('GET', f'/accounts/{account_id}/pages/projects/{project_name}', token)
if result.get('success'):
return True
print(f"项目 {project_name} 不存在,正在创建...")
result = api_request('POST', f'/accounts/{account_id}/pages/projects', token,
data={"name": project_name, "production_branch": "main"})
if result.get('success'):
print(f"✅ 项目 {project_name} 创建成功")
return True
print(f"❌ 创建项目失败: {result.get('errors', [])}", file=sys.stderr)
sys.exit(1)
def _upload_bucket(bucket, jwt):
"""上传一批文件"""
body = json.dumps(bucket).encode('utf-8')
headers = {"Authorization": f"Bearer {jwt}", "Content-Type": "application/json"}
req = urllib.request.Request(f"{CF_API}/pages/assets/upload", data=body, headers=headers, method='POST')
try:
with urllib.request.urlopen(req, timeout=120) as resp:
result = json.loads(resp.read().decode('utf-8'))
if not result.get('success', True):
print(f"⚠️ 上传警告: {result}", file=sys.stderr)
except urllib.error.HTTPError as e:
print(f"❌ 上传失败: HTTP {e.code} - {e.read().decode('utf-8')}", file=sys.stderr)
sys.exit(1)
def deploy_files(token, account_id, project_name, files, branch=None):
"""通过 API 部署文件到 Cloudflare Pages"""
# 获取上传凭证
print("📤 获取上传凭证...")
upload_result = api_request('GET', f'/accounts/{account_id}/pages/projects/{project_name}/upload-token', token)
if not upload_result.get('success'):
print(f"❌ 获取上传凭证失败: {upload_result.get('errors', [])}", file=sys.stderr)
sys.exit(1)
upload_jwt = upload_result['result']['jwt']
# 上传文件(分批,每批不超过 50MB)
print(f"📦 上传 {len(files)} 个文件...")
bucket, bucket_size, all_hashes = [], 0, []
for path_key, info in files.items():
item = {"key": info['hash'], "value": info['content_b64'],
"metadata": {"contentType": info['content_type']}, "base64": True}
all_hashes.append(info['hash'])
if bucket_size + info['size'] > 50 * 1024 * 1024 and bucket:
_upload_bucket(bucket, upload_jwt)
bucket, bucket_size = [], 0
bucket.append(item)
bucket_size += info['size']
if bucket:
_upload_bucket(bucket, upload_jwt)
# 确认上传
print("🔄 确认文件上传...")
api_request('POST', '/pages/assets/upsert-hashes', upload_jwt, data={"hashes": all_hashes})
# 创建部署
print("🚀 创建部署...")
manifest = {k: v['hash'] for k, v in files.items()}
fields = {"manifest": json.dumps(manifest)}
if branch:
fields["branch"] = branch
result = multipart_request(
f"{CF_API}/accounts/{account_id}/pages/projects/{project_name}/deployments", token, fields)
if result.get('success') or result.get('id'):
url = result.get('url', '') or result.get('result', {}).get('url', '')
print(f"\n✅ 部署成功!")
print(f"🌐 访问: https://{project_name}.pages.dev")
if url:
print(f"🔗 部署预览: {url}")
else:
print(f"\n❌ 部署失败: {result.get('errors', [])}", file=sys.stderr)
sys.exit(1)
def cmd_deploy(args):
"""部署命令"""
token = load_config()
account_id = get_account_id(token)
source = os.path.abspath(args.source)
if not os.path.exists(source):
print(f"路径不存在: {source}", file=sys.stderr)
sys.exit(1)
print(f"正在部署到 Cloudflare Pages...")
print(f"项目: {args.project}")
print(f"源: {source}")
if args.branch:
print(f"分支: {args.branch}")
print()
ensure_project(token, account_id, args.project)
files = collect_files(source)
print(f"共 {len(files)} 个文件待部署")
for path_key in files:
print(f" 📄 {path_key} ({files[path_key]['size']} bytes)")
print()
deploy_files(token, account_id, args.project, files, branch=args.branch)
def cmd_list(args):
"""列出所有项目"""
token = load_config()
account_id = get_account_id(token)
result = api_request('GET', f'/accounts/{account_id}/pages/projects', token)
if not result.get('success'):
print(f"API 错误: {result.get('errors', [])}", file=sys.stderr)
sys.exit(1)
projects = result.get('result', [])
if args.json:
print(json.dumps(projects, ensure_ascii=False, indent=2))
return
if not projects:
print("暂无项目")
return
print(f"共 {len(projects)} 个项目:\n")
for p in projects:
print(f" 📦 {p.get('name', '?')}")
print(f" 🌐 https://{p.get('subdomain', '?')}")
print(f" 📅 创建于 {p.get('created_on', '?')[:10]}")
domains = p.get('domains', [])
if domains:
print(f" 🔗 域名: {', '.join(domains)}")
print()
def cmd_deployments(args):
"""查看部署历史"""
token = load_config()
account_id = get_account_id(token)
result = api_request('GET', f'/accounts/{account_id}/pages/projects/{args.project}/deployments', token)
if not result.get('success'):
print(f"API 错误: {result.get('errors', [])}", file=sys.stderr)
sys.exit(1)
deployments = result.get('result', [])
if args.json:
print(json.dumps(deployments, ensure_ascii=False, indent=2))
return
if not deployments:
print(f"项目 {args.project} 暂无部署记录")
return
print(f"项目 {args.project} 的部署记录 (共 {len(deployments)} 条):\n")
for d in deployments[:10]:
status = d.get('latest_stage', {}).get('status', '?')
emoji = '✅' if status == 'success' else '❌' if status == 'failure' else '⏳'
print(f" {emoji} {d.get('id', '?')[:12]}...")
print(f" 🌐 {d.get('url', '?')}")
print(f" 📅 {d.get('created_on', '?')[:19].replace('T', ' ')} | 🏷️ {d.get('environment', '?')} | 状态: {status}")
print()
def main():
parser = argparse.ArgumentParser(
description='Cloudflare Pages 部署工具(纯 API,无需 wrangler)',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
%(prog)s deploy index.html --project my-site
%(prog)s deploy ./dist --project my-site
%(prog)s deploy ./dist --project my-site --branch preview
%(prog)s list
%(prog)s deployments --project my-site
""")
subparsers = parser.add_subparsers(dest='command', help='命令')
dp = subparsers.add_parser('deploy', help='部署网页到 Cloudflare Pages')
dp.add_argument('source', help='HTML 文件或目录路径')
dp.add_argument('--project', required=True, help='Cloudflare Pages 项目名')
dp.add_argument('--branch', help='部署分支(用于预览环境)')
lp = subparsers.add_parser('list', help='列出所有项目')
lp.add_argument('--json', action='store_true', help='JSON 格式输出')
hp = subparsers.add_parser('deployments', help='查看部署历史')
hp.add_argument('--project', required=True, help='项目名')
hp.add_argument('--json', action='store_true', help='JSON 格式输出')
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
{'deploy': cmd_deploy, 'list': cmd_list, 'deployments': cmd_deployments}[args.command](args)
if __name__ == '__main__':
main()