胡焦24

You got a dream, you gotta to protect it!

站内搜索

选择搜索引擎,输入关键词开始搜索

Google
Bing
Yahoo
百度
💡 小贴士:选择不同的搜索引擎可能会得到不同的搜索结果

使用wrangler发布cf的workers项目

发布日期:2025-11-13 |文章分类: 默认分类

前言

想必大家都知道,在 cf 上可以免费搭建 pages 静态站点,比如我的博客 https://blog.qc7.org 就是在 github 上提交,然后在 cf 上部署的

但是有些人可能还不知道,cf 还提供了免费的数据库,对大部分个人用户来说,这个免费的额度已经足够使用了,在数据库的基础上,就可以实现动态的 web 服务了

与传统的 web 服务部同,cf 提供的基于 node 的 wrangler 开发部署模式,不支持传统的 php、java、net 等语言的动态 web 服务

本地安装wrangler

wrangler 是 cf 发布的一个 nodejs 组件包,在本地使用下面命令安装

npm install -g wrangler

全局包安装到 node 路径下的 node_modules 目录中,然后在 node 路径中添加命令脚本,对于非全局包,会安装到项目下的 node_modules 目录中

安装完毕后,在本地 cmd 窗口中运行 wrnagler 命令,显示它的帮助信息,表示安装成功

wrangler项目完整流程

控制台输入 wrangler init hello 一路回车创建一个默认的 hello 项目,最后两步可以选择 No,表示不使用 git 版本控制,不自动发布到 cf (使用手动发布)

创建完毕后,使用 vscode 打开项目,显示的目录结构以及构建脚本如下

命令行中输入 npm run dev 就可以在本地运行起来了,监听在 http://127.0.0.1:8787,使用浏览器输入 url 地址进行访问

默认 wrangler 创建的 example 的页面显示如下

使用 npm run deploy 命令可以将项目发布到 cf 中,初次发布的时候会弹出一个授权页面,在页面上登录你的 cf 账号授权即可

发布显示的信息如下,也可以通过日志中提示的域名进行访问 https://hello.?????.workers.dev/,这和正常的 web 服务没任何区别,还能自行绑定个人域名

创建数据库和表

cf 提供多种数据库,常见的有 D1(关系数据库,类似sqlite轻量的)、Workers KV(类似redis的键值存储)、R2(类似aws s3的对象存储),其他类型的一般用不到

官方这里有完整的指导文档 https://developers.cloudflare.com/d1/get-started/

通过以下命令创建一个类型为 d1 名为 prod-d1-tutorial 的数据库,前面已经完成授权,这里将在 cf 中创建数据库,并将配置添加到 wrangler.jsonc 文件中

生成配置中的 remote 字段为 true 表示使用远端的数据库

npx wrangler@latest d1 create prod-d1-tutorial

在前面创建好的数据库基础上,创建一个 Customers 表,并添加几条记录,数据库脚本如下

DROP TABLE IF EXISTS Customers;
CREATE TABLE IF NOT EXISTS Customers (
    CustomerId INTEGER PRIMARY KEY AUTOINCREMENT,
    CompanyName TEXT NOT NULL,
    ContactName TEXT NOT NULL,
    Email TEXT,
    Phone TEXT,
    CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO Customers (CompanyName, ContactName, Email, Phone) VALUES
    ('Alfreds Futterkiste', 'Maria Anders', '[email protected]', '123-456-7890'),
    ('Around the Horn', 'Thomas Hardy', '[email protected]', '123-456-7891'),
    ('Bs Beverages', 'Victoria Ashworth', '[email protected]', '123-456-7892'),
    ('Bs Beverages', 'Random Name', '[email protected]', '123-456-7893');

将脚本保存为 schema.sql,然后运行以下命令,这里指示为 --remote 表示在远程运行,如果本地运行,则使用 --local

npx wrangler d1 execute prod-d1-tutorial --remote --file=./schema.sql

上述 sql 执行后,可以通过以下命令查看 d1 中的数据内容

npx wrangler d1 execute prod-d1-tutorial --remote --command="SELECT * FROM Customers"` 

也可以在 cf 的 web 页面上查看数据记录

代码读写表记录

前面已经创建了数据库和表,并插入了一些数据,现在实现一个简单的页面,对数据库进行增删改用户数据,测试后将代码通过 npm run deploy 发布到 cf 中

发布的最终效果如下,可以实现新增记录、修改记录、删除记录

项目原有两个代码文件 public/index.htmlsrc/index.ts,修改后最终的代码如下

<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>D1 CRM - 现代化客户管理系统</title>
    <script src="https://unpkg.com/[email protected]"></script>
    <script src="https://unpkg.com/[email protected]"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
    <style>
        :root {
            --primary: #6366f1;
            --primary-dark: #4f46e5;
            --secondary: #f8fafc;
            --accent: #f59e0b;
            --text: #1e293b;
            --text-light: #64748b;
            --border: #e2e8f0;
            --success: #10b981;
            --warning: #f59e0b;
            --error: #ef4444;
            --shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
            --radius: 12px;
            --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        }

        [data-theme="dark"] {
            --primary: #818cf8;
            --primary-dark: #6366f1;
            --secondary: #1e293b;
            --accent: #fbbf24;
            --text: #f1f5f9;
            --text-light: #94a3b8;
            --border: #334155;
            --shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            color: var(--text);
            line-height: 1.6;
        }

        .app-container {
            min-height: 100vh;
            display: flex;
            flex-direction: column;
        }

        /* Header */
        .header {
            background: rgba(255, 255, 255, 0.95);
            backdrop-filter: blur(20px);
            border-bottom: 1px solid var(--border);
            padding: 1rem 2rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
            box-shadow: var(--shadow);
        }

        [data-theme="dark"] .header {
            background: rgba(30, 41, 59, 0.95);
        }

        .logo {
            display: flex;
            align-items: center;
            gap: 0.75rem;
            font-size: 1.5rem;
            font-weight: 700;
            color: var(--primary);
        }

        .theme-toggle {
            background: none;
            border: none;
            color: var(--text-light);
            cursor: pointer;
            padding: 0.5rem;
            border-radius: var(--radius);
            transition: var(--transition);
        }

        .theme-toggle:hover {
            color: var(--text);
            background: var(--secondary);
        }

        /* Main Content */
        .main-content {
            flex: 1;
            padding: 2rem;
            max-width: 1400px;
            margin: 0 auto;
            width: 100%;
        }

        .dashboard {
            display: grid;
            gap: 2rem;
            grid-template-columns: 1fr 400px;
        }

        @media (max-width: 1024px) {
            .dashboard {
                grid-template-columns: 1fr;
            }
        }

        /* Cards */
        .card {
            background: rgba(255, 255, 255, 0.95);
            backdrop-filter: blur(20px);
            border-radius: var(--radius);
            padding: 1.5rem;
            box-shadow: var(--shadow);
            border: 1px solid var(--border);
            transition: var(--transition);
        }

        [data-theme="dark"] .card {
            background: rgba(30, 41, 59, 0.95);
        }

        .card:hover {
            transform: translateY(-2px);
            box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.15);
        }

        .card-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 1.5rem;
        }

        .card-title {
            font-size: 1.25rem;
            font-weight: 600;
            color: var(--text);
        }

        /* Form Styles */
        .form-grid {
            display: grid;
            gap: 1rem;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
        }

        .form-group {
            display: flex;
            flex-direction: column;
            gap: 0.5rem;
        }

        .form-label {
            font-weight: 500;
            color: var(--text);
            font-size: 0.875rem;
        }

        .form-input {
            padding: 0.75rem 1rem;
            border: 2px solid var(--border);
            border-radius: var(--radius);
            background: var(--secondary);
            color: var(--text);
            transition: var(--transition);
            font-size: 0.875rem;
        }

        .form-input:focus {
            outline: none;
            border-color: var(--primary);
            box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
        }

        /* Button Styles */
        .btn {
            padding: 0.75rem 1.5rem;
            border: none;
            border-radius: var(--radius);
            font-weight: 500;
            font-size: 0.875rem;
            cursor: pointer;
            transition: var(--transition);
            display: inline-flex;
            align-items: center;
            gap: 0.5rem;
            text-decoration: none;
        }

        .btn-primary {
            background: var(--primary);
            color: white;
        }

        .btn-primary:hover {
            background: var(--primary-dark);
            transform: translateY(-1px);
        }

        .btn-success {
            background: var(--success);
            color: white;
        }

        .btn-warning {
            background: var(--warning);
            color: white;
        }

        .btn-danger {
            background: var(--error);
            color: white;
        }

        .btn-outline {
            background: transparent;
            border: 2px solid var(--border);
            color: var(--text);
        }

        .btn-sm {
            padding: 0.5rem 1rem;
            font-size: 0.75rem;
        }

        /* Table Styles */
        .table-container {
            overflow-x: auto;
            border-radius: var(--radius);
        }

        .data-table {
            width: 100%;
            border-collapse: collapse;
            background: var(--secondary);
        }

        .data-table th,
        .data-table td {
            padding: 1rem;
            text-align: left;
            border-bottom: 1px solid var(--border);
        }

        .data-table th {
            background: var(--secondary);
            font-weight: 600;
            font-size: 0.875rem;
            color: var(--text-light);
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }

        .data-table tr:hover {
            background: rgba(99, 102, 241, 0.05);
        }

        .actions-cell {
            display: flex;
            gap: 0.5rem;
        }

        /* Status Indicators */
        .status-badge {
            padding: 0.25rem 0.75rem;
            border-radius: 20px;
            font-size: 0.75rem;
            font-weight: 500;
        }

        .status-active {
            background: rgba(16, 185, 129, 0.1);
            color: var(--success);
        }

        /* Loading States */
        .skeleton {
            background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
            background-size: 200% 100%;
            animation: loading 1.5s infinite;
            border-radius: 4px;
        }

        @keyframes loading {
            0% { background-position: 200% 0; }
            100% { background-position: -200% 0; }
        }

        /* Toast Notifications */
        .toast-container {
            position: fixed;
            top: 2rem;
            right: 2rem;
            z-index: 1000;
        }

        .toast {
            background: var(--secondary);
            border-left: 4px solid var(--primary);
            padding: 1rem;
            border-radius: var(--radius);
            box-shadow: var(--shadow);
            margin-bottom: 1rem;
            animation: slideIn 0.3s ease-out;
        }

        @keyframes slideIn {
            from { transform: translateX(100%); opacity: 0; }
            to { transform: translateX(0); opacity: 1; }
        }

        /* Responsive */
        @media (max-width: 768px) {
            .main-content {
                padding: 1rem;
            }

            .header {
                padding: 1rem;
            }

            .form-grid {
                grid-template-columns: 1fr;
            }

            .actions-cell {
                flex-direction: column;
            }
        }
    </style>
</head>
<body>
    <div class="app-container" id="app">
        <!-- Header -->
        <header class="header">
            <div class="logo">
                <i class="fas fa-database"></i>
                <span>D1 CRM</span>
            </div>
            <button class="theme-toggle" onclick="toggleTheme()">
                <i class="fas fa-moon"></i>
            </button>
        </header>

        <!-- Main Content -->
        <main class="main-content">
            <div class="dashboard">
                <!-- Customer Form -->
                <div class="card">
                    <div class="card-header">
                        <h2 class="card-title" id="formTitle">
                            <i class="fas fa-user-plus"></i>
                            添加新客户
                        </h2>
                    </div>
                    <form id="customerForm" _="on submit halt the event then call submitCustomerForm()">
                        <input type="hidden" id="customerId">
                        <div class="form-grid">
                            <div class="form-group">
                                <label class="form-label" for="CompanyName">
                                    <i class="fas fa-building"></i> 公司名称 *
                                </label>
                                <input type="text" id="CompanyName" class="form-input" required
                                       placeholder="输入公司名称">
                            </div>
                            <div class="form-group">
                                <label class="form-label" for="ContactName">
                                    <i class="fas fa-user"></i> 联系人 *
                                </label>
                                <input type="text" id="ContactName" class="form-input" required
                                       placeholder="输入联系人姓名">
                            </div>
                            <div class="form-group">
                                <label class="form-label" for="Email">
                                    <i class="fas fa-envelope"></i> 邮箱
                                </label>
                                <input type="Email" id="Email" class="form-input"
                                       placeholder="[email protected]">
                            </div>
                            <div class="form-group">
                                <label class="form-label" for="Phone">
                                    <i class="fas fa-Phone"></i> 电话
                                </label>
                                <input type="tel" id="Phone" class="form-input"
                                       placeholder="+86 138 0000 0000">
                            </div>
                        </div>
                        <div style="display: flex; gap: 1rem; margin-top: 1.5rem; flex-wrap: wrap;">
                            <button type="submit" class="btn btn-primary" id="submitBtn">
                                <i class="fas fa-plus"></i> 添加客户
                            </button>
                            <button type="submit" class="btn btn-success" id="updateBtn" style="display: none;">
                                <i class="fas fa-save"></i> 更新客户
                            </button>
                            <button type="button" class="btn btn-outline" onclick="resetForm()">
                                <i class="fas fa-times"></i> 取消编辑
                            </button>
                        </div>
                    </form>
                </div>

                <!-- Quick Actions -->
                <div class="card">
                    <div class="card-header">
                        <h2 class="card-title">
                            <i class="fas fa-bolt"></i> 快速操作
                        </h2>
                    </div>
                    <div style="display: flex; flex-direction: column; gap: 1rem;">
                        <button class="btn btn-outline" onclick="loadCustomers()">
                            <i class="fas fa-sync-alt"></i> 刷新数据
                        </button>
                        <button class="btn btn-warning" onclick="exportData()">
                            <i class="fas fa-download"></i> 导出数据
                        </button>
                        <button class="btn btn-danger" onclick="confirmDeleteAll()">
                            <i class="fas fa-trash"></i> 清空数据
                        </button>
                    </div>
                </div>
            </div>

            <!-- Customer Table -->
            <div class="card" style="margin-top: 2rem;">
                <div class="card-header">
                    <h2 class="card-title">
                        <i class="fas fa-users"></i> 客户列表
                        <span class="status-badge status-active" id="customerCount">加载中...</span>
                    </h2>
                    <div style="display: flex; gap: 1rem; align-items: center;">
                        <div class="form-group" style="flex-direction: row; align-items: center;">
                            <label class="form-label" for="search">
                                <i class="fas fa-search"></i>
                            </label>
                            <input type="text" id="search" class="form-input"
                                   placeholder="搜索客户..." oninput="filterCustomers()"
                                   style="width: 200px;">
                        </div>
                    </div>
                </div>
                <div class="table-container">
                    <table class="data-table" id="customersTable">
                        <thead>
                            <tr>
                                <th>ID</th>
                                <th>公司名称</th>
                                <th>联系人</th>
                                <th>邮箱</th>
                                <th>电话</th>
                                <th>创建时间</th>
                                <th>操作</th>
                            </tr>
                        </thead>
                        <tbody id="customersTableBody">
                            <tr>
                                <td colspan="7" style="text-align: center; padding: 3rem;">
                                    <div class="skeleton" style="height: 20px; margin-bottom: 0.5rem;"></div>
                                    <div class="skeleton" style="height: 20px; width: 60%; margin: 0 auto;"></div>
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </main>

        <!-- Toast Container -->
        <div class="toast-container" id="toastContainer"></div>
    </div>

    <script>
        // 现代化 JavaScript 代码
        class D1CRM {
            constructor() {
                this.currentEditingId = null;
                this.customers = [];
                this.init();
            }

            async init() {
                await this.loadCustomers();
                this.setupEventListeners();
                this.showToast('系统初始化完成', 'success');
            }

            setupEventListeners() {
                // 表单提交事件
                document.getElementById('customerForm').addEventListener('submit', (e) => {
                    e.preventDefault();
                    this.submitCustomerForm();
                });

                // 实时搜索
                document.getElementById('search').addEventListener('input', (e) => {
                    this.filterCustomers(e.target.value);
                });
            }

            async loadCustomers() {
                try {
                    this.showLoading();
                    const response = await fetch('/api/customers');

                    if (!response.ok) throw new Error('获取数据失败');

					const result = await response.json();

					if (result && result.success && Array.isArray(result.data)) {
						this.customers = result.data;
					} else {
						this.customers = [];
						console.warn('Unexpected API response format:', result);
					}

                    this.renderCustomers();
                    this.updateCustomerCount();

                } catch (error) {
                    this.showToast(`加载失败: ${error.message}`, 'error');
                }
            }

            renderCustomers(customers = this.customers) {
                const tbody = document.getElementById('customersTableBody');

                if (customers.length === 0) {
                    tbody.innerHTML = `
                        <tr>
                            <td colspan="7" style="text-align: center; padding: 3rem; color: var(--text-light);">
                                <i class="fas fa-inbox" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.5;"></i>
                                <div>暂无客户数据</div>
                            </td>
                        </tr>
                    `;
                    return;
                }

                tbody.innerHTML = customers.map(customer => `
                    <tr>
                        <td>${customer.CustomerId}</td>
                        <td>
                            <div style="font-weight: 600;">${customer.CompanyName}</div>
                        </td>
                        <td>${customer.ContactName}</td>
                        <td>${customer.Email || '<span style="opacity: 0.5;">未设置</span>'}</td>
                        <td>${customer.Phone || '<span style="opacity: 0.5;">未设置</span>'}</td>
                        <td>${new Date(customer.CreatedAt).toLocaleDateString('zh-CN')}</td>
                        <td class="actions-cell">
                            <button class="btn btn-outline btn-sm"
                                    onclick="app.editCustomer(${customer.CustomerId})"
                                    title="编辑">
                                <i class="fas fa-edit"></i>
                            </button>
                            <button class="btn btn-danger btn-sm"
                                    onclick="app.deleteCustomer(${customer.CustomerId})"
                                    title="删除">
                                <i class="fas fa-trash"></i>
                            </button>
                        </td>
                    </tr>
                `).join('');
            }

            async submitCustomerForm() {
                const formData = this.getFormData();

                if (!this.validateForm(formData)) return;

                try {
                    const url = this.currentEditingId
                        ? `/api/customers/${this.currentEditingId}`
                        : '/api/customers';

                    const method = this.currentEditingId ? 'PUT' : 'POST';

                    const response = await fetch(url, {
                        method,
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(formData)
                    });

                    if (!response.ok) throw new Error('操作失败');

                    const result = await response.json();

                    this.showToast(
                        this.currentEditingId ? '客户更新成功' : '客户添加成功',
                        'success'
                    );

                    this.resetForm();
                    await this.loadCustomers();

                } catch (error) {
                    this.showToast(`操作失败: ${error.message}`, 'error');
                }
            }

            async editCustomer(id) {
                try {
                    const response = await fetch(`/api/customers/${id}`);
                    if (!response.ok) throw new Error('获取客户信息失败');

					const result = await response.json();

					if (!result || !result.success) {
						throw new Error('客户信息格式错误');
					}

					const customer = result.data;

					if (!customer || !customer.CompanyName) {
						throw new Error('客户数据不完整');
					}

                    this.populateForm(customer);
                    this.showToast(`正在编辑: ${customer.CompanyName}`, 'info');

                } catch (error) {
                    this.showToast(`编辑失败: ${error.message}`, 'error');
                }
            }

            async deleteCustomer(id) {
                if (!confirm('确定要删除这个客户吗?此操作不可恢复!')) return;

                try {
                    const response = await fetch(`/api/customers/${id}`, {
                        method: 'DELETE'
                    });

                    if (!response.ok) throw new Error('删除失败');

                    this.showToast('客户删除成功', 'success');
                    await this.loadCustomers();

                } catch (error) {
                    this.showToast(`删除失败: ${error.message}`, 'error');
                }
            }

            // 辅助方法
            getFormData() {
                return {
                    CompanyName: document.getElementById('CompanyName').value.trim(),
                    ContactName: document.getElementById('ContactName').value.trim(),
                    Email: document.getElementById('Email').value.trim(),
                    Phone: document.getElementById('Phone').value.trim()
                };
            }

            validateForm(data) {
                if (!data.CompanyName || !data.ContactName) {
                    this.showToast('公司名称和联系人为必填项', 'warning');
                    return false;
                }
                return true;
            }

            populateForm(customer) {
                document.getElementById('customerId').value = customer.CustomerId;
                document.getElementById('CompanyName').value = customer.CompanyName;
                document.getElementById('ContactName').value = customer.ContactName;
                document.getElementById('Email').value = customer.Email || '';
                document.getElementById('Phone').value = customer.Phone || '';

                this.currentEditingId = customer.CustomerId;
                document.getElementById('submitBtn').style.display = 'none';
                document.getElementById('updateBtn').style.display = 'inline-flex';
                document.getElementById('formTitle').innerHTML =
                    '<i class="fas fa-edit"></i> 编辑客户';
            }

            resetForm() {
                document.getElementById('customerForm').reset();
                this.currentEditingId = null;
                document.getElementById('submitBtn').style.display = 'inline-flex';
                document.getElementById('updateBtn').style.display = 'none';
                document.getElementById('formTitle').innerHTML =
                    '<i class="fas fa-user-plus"></i> 添加新客户';
            }

            filterCustomers(searchTerm = '') {
                const filtered = this.customers.filter(customer =>
                    customer.CompanyName.toLowerCase().includes(searchTerm.toLowerCase()) ||
                    customer.ContactName.toLowerCase().includes(searchTerm.toLowerCase()) ||
                    (customer.Email && customer.Email.toLowerCase().includes(searchTerm.toLowerCase()))
                );
                this.renderCustomers(filtered);
            }

            updateCustomerCount() {
                document.getElementById('customerCount').textContent =
                    `${this.customers.length} 个客户`;
            }

            showLoading() {
                const tbody = document.getElementById('customersTableBody');
                tbody.innerHTML = `
                    <tr>
                        <td colspan="7" style="text-align: center; padding: 2rem;">
                            <i class="fas fa-spinner fa-spin" style="font-size: 2rem; color: var(--primary);"></i>
                            <div style="margin-top: 1rem; color: var(--text-light);">加载中...</div>
                        </td>
                    </tr>
                `;
            }

            showToast(message, type = 'info') {
                const toastContainer = document.getElementById('toastContainer');
                const toast = document.createElement('div');
                toast.className = 'toast';
                toast.innerHTML = `
                    <div style="display: flex; align-items: center; gap: 0.5rem;">
                        <i class="fas fa-${this.getToastIcon(type)}"
                           style="color: var(--${type});"></i>
                        <span>${message}</span>
                    </div>
                `;

                toastContainer.appendChild(toast);

                setTimeout(() => {
                    toast.remove();
                }, 3000);
            }

            getToastIcon(type) {
                const icons = {
                    success: 'check-circle',
                    error: 'exclamation-circle',
                    warning: 'exclamation-triangle',
                    info: 'info-circle'
                };
                return icons[type] || 'info-circle';
            }
        }

        // 全局函数
        function toggleTheme() {
            const currentTheme = document.documentElement.getAttribute('data-theme');
            const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
            document.documentElement.setAttribute('data-theme', newTheme);

            const icon = document.querySelector('.theme-toggle i');
            icon.className = newTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';

            localStorage.setItem('theme', newTheme);
        }

        function confirmDeleteAll() {
            if (!confirm('确定要删除所有客户数据吗?此操作不可恢复!')) return;
            app.deleteAllCustomers();
        }

        async function exportData() {
            // 简单的导出功能
            const data = JSON.stringify(app.customers, null, 2);
            const blob = new Blob([data], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `customers-${new Date().toISOString().split('T')[0]}.json`;
            a.click();
            URL.revokeObjectURL(url);
            app.showToast('数据导出成功', 'success');
        }

        // 初始化应用
        const app = new D1CRM();

        // 设置主题
        const savedTheme = localStorage.getItem('theme') || 'light';
        document.documentElement.setAttribute('data-theme', savedTheme);
    </script>
</body>
</html>
// 类型定义
interface Customer {
	CustomerId?: number;
	CompanyName: string;
	ContactName: string;
	Email?: string;
	Phone?: string;
	CreatedAt?: string;
  }

  interface ApiResponse<T = any> {
	success: boolean;
	data?: T;
	error?: string;
	message?: string;
	metadata?: {
	  total?: number;
	  page?: number;
	  limit?: number;
	};
  }

  // 环境变量类型
  export interface Env {
	prod_d1_tutorial: D1Database;
  }

  // 错误处理类
  class AppError extends Error {
	constructor(
	  message: string,
	  public statusCode: number = 500,
	  public code?: string
	) {
	  super(message);
	  this.name = 'AppError';
	}
  }

  // 响应工具类
  class ResponseBuilder {
	static json<T>(data: T, status: number = 200, headers?: Record<string, string>): Response {
	  const response: ApiResponse<T> = {
		success: status < 400,
		data
	  };

	  return new Response(JSON.stringify(response), {
		status,
		headers: {
		  'Content-Type': 'application/json',
		  'Access-Control-Allow-Origin': '*',
		  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
		  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
		  ...headers,
		},
	  });
	}

	static error(error: string, status: number = 500, code?: string): Response {
	  const response: ApiResponse = {
		success: false,
		error,
		...(code && { code })
	  };

	  return new Response(JSON.stringify(response), {
		status,
		headers: {
		  'Content-Type': 'application/json',
		  'Access-Control-Allow-Origin': '*',
		  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
		  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
		},
	  });
	}

	static success(message: string, data?: any): Response {
	  const response: ApiResponse = {
		success: true,
		message,
		...(data && { data })
	  };

	  return ResponseBuilder.json(response, 200);
	}
  }

  // 验证工具类
  class Validator {
	static validateCustomer(data: any): Partial<Customer> {
	  const errors: string[] = [];

	  if (!data.CompanyName || typeof data.CompanyName !== 'string' || data.CompanyName.trim().length === 0) {
		errors.push('CompanyName is required and must be a non-empty string');
	  }

	  if (!data.ContactName || typeof data.ContactName !== 'string' || data.ContactName.trim().length === 0) {
		errors.push('ContactName is required and must be a non-empty string');
	  }

	  if (data.Email && typeof data.Email !== 'string') {
		errors.push('Email must be a string');
	  }

	  if (data.Phone && typeof data.Phone !== 'string') {
		errors.push('Phone must be a string');
	  }

	  if (errors.length > 0) {
		throw new AppError(`Validation failed: ${errors.join(', ')}`, 400, 'VALIDATION_ERROR');
	  }

	  return {
		CompanyName: data.CompanyName.trim(),
		ContactName: data.ContactName.trim(),
		Email: data.Email ? data.Email.trim() : null,
		Phone: data.Phone ? data.Phone.trim() : null,
	  };
	}
  }

  // 数据库服务类
  class CustomerService {
	constructor(private db: D1Database) {}

	async getAllCustomers(): Promise<Customer[]> {
	  try {
		const query = `
		  SELECT
			CustomerId,
			CompanyName,
			ContactName,
			Email,
			Phone,
			CreatedAt
		  FROM Customers
		  ORDER BY CustomerId DESC
		`;

		const result = await this.db.prepare(query).all();
		return result.results as Customer[];
	  } catch (error) {
		console.error('Database error in getAllCustomers:', error);
		throw new AppError('Failed to fetch customers', 500, 'DATABASE_ERROR');
	  }
	}

	async getCustomerById(id: number): Promise<Customer | null> {
	  try {
		const query = `
		  SELECT
			CustomerId,
			CompanyName,
			ContactName,
			Email,
			Phone,
			CreatedAt
		  FROM Customers
		  WHERE CustomerId = ?
		`;

		const result = await this.db.prepare(query).bind(id).all();
		return result.results.length > 0 ? result.results[0] as Customer : null;
	  } catch (error) {
		console.error('Database error in getCustomerById:', error);
		throw new AppError('Failed to fetch customer', 500, 'DATABASE_ERROR');
	  }
	}

	async createCustomer(customerData: Omit<Customer, 'CustomerId'>): Promise<{ id: number }> {
	  try {
		const query = `
		  INSERT INTO Customers (CompanyName, ContactName, Email, Phone)
		  VALUES (?, ?, ?, ?)
		`;

		const result = await this.db
		  .prepare(query)
		  .bind(
			customerData.CompanyName,
			customerData.ContactName,
			customerData.Email || null,
			customerData.Phone || null
		  )
		  .run();

		return { id: Number(result.meta.last_row_id) };
	  } catch (error) {
		console.error('Database error in createCustomer:', error);
		throw new AppError('Failed to create customer', 500, 'DATABASE_ERROR');
	  }
	}

	async updateCustomer(id: number, customerData: Partial<Customer>): Promise<{ changes: number }> {
	  try {
		const query = `
		  UPDATE Customers
		  SET CompanyName = ?, ContactName = ?, Email = ?, Phone = ?
		  WHERE CustomerId = ?
		`;

		const result = await this.db
		  .prepare(query)
		  .bind(
			customerData.CompanyName,
			customerData.ContactName,
			customerData.Email || null,
			customerData.Phone || null,
			id
		  )
		  .run();

		return { changes: result.meta.changes || 0 };
	  } catch (error) {
		console.error('Database error in updateCustomer:', error);
		throw new AppError('Failed to update customer', 500, 'DATABASE_ERROR');
	  }
	}

	async deleteCustomer(id: number): Promise<{ changes: number }> {
	  try {
		const query = 'DELETE FROM Customers WHERE CustomerId = ?';
		const result = await this.db.prepare(query).bind(id).run();
		return { changes: result.meta.changes || 0 };
	  } catch (error) {
		console.error('Database error in deleteCustomer:', error);
		throw new AppError('Failed to delete customer', 500, 'DATABASE_ERROR');
	  }
	}

	async deleteAllCustomers(): Promise<{ changes: number }> {
	  try {
		const query = 'DELETE FROM Customers';
		const result = await this.db.prepare(query).run();
		return { changes: result.meta.changes || 0 };
	  } catch (error) {
		console.error('Database error in deleteAllCustomers:', error);
		throw new AppError('Failed to delete all customers', 500, 'DATABASE_ERROR');
	  }
	}
  }

  // 路由处理器类
  class CustomerHandler {
	private service: CustomerService;

	constructor(private env: Env) {
	  this.service = new CustomerService(env.prod_d1_tutorial);
	}

	async handleRequest(request: Request, pathname: string, customerId?: number): Promise<Response> {
	  try {
		switch (request.method) {
		  case 'GET':
			if (customerId) {
			  return await this.getCustomer(customerId);
			} else {
			  return await this.getAllCustomers();
			}

		  case 'POST':
			if (!customerId) {
			  return await this.createCustomer(request);
			}
			break;

		  case 'PUT':
			if (customerId) {
			  return await this.updateCustomer(request, customerId);
			}
			break;

		  case 'DELETE':
			if (customerId) {
			  return await this.deleteCustomer(customerId);
			} else {
			  return await this.deleteAllCustomers();
			}

		  default:
			return ResponseBuilder.error('Method not allowed', 405);
		}

		return ResponseBuilder.error('Not found', 404);
	  } catch (error) {
		return this.handleError(error);
	  }
	}

	private async getAllCustomers(): Promise<Response> {
	  const customers = await this.service.getAllCustomers();
	  return ResponseBuilder.json(customers, 200, {
		'Cache-Control': 'no-cache',
	  });
	}

	private async getCustomer(id: number): Promise<Response> {
	  const customer = await this.service.getCustomerById(id);

	  if (!customer) {
		return ResponseBuilder.error('Customer not found', 404, 'NOT_FOUND');
	  }

	  return ResponseBuilder.json(customer);
	}

	private async createCustomer(request: Request): Promise<Response> {
	  let body: any;

	  try {
		body = await request.json();
	  } catch {
		return ResponseBuilder.error('Invalid JSON body', 400, 'INVALID_JSON');
	  }

	  const validatedData = Validator.validateCustomer(body);
	  const result = await this.service.createCustomer(validatedData);

	  return ResponseBuilder.success('Customer created successfully', { id: result.id });
	}

	private async updateCustomer(request: Request, id: number): Promise<Response> {
	  // 检查客户是否存在
	  const existingCustomer = await this.service.getCustomerById(id);
	  if (!existingCustomer) {
		return ResponseBuilder.error('Customer not found', 404, 'NOT_FOUND');
	  }

	  let body: any;

	  try {
		body = await request.json();
	  } catch {
		return ResponseBuilder.error('Invalid JSON body', 400, 'INVALID_JSON');
	  }

	  const validatedData = Validator.validateCustomer(body);
	  const result = await this.service.updateCustomer(id, validatedData);

	  if (result.changes === 0) {
		return ResponseBuilder.error('No changes made', 400, 'NO_CHANGES');
	  }

	  return ResponseBuilder.success('Customer updated successfully');
	}

	private async deleteCustomer(id: number): Promise<Response> {
	  // 检查客户是否存在
	  const existingCustomer = await this.service.getCustomerById(id);
	  if (!existingCustomer) {
		return ResponseBuilder.error('Customer not found', 404, 'NOT_FOUND');
	  }

	  const result = await this.service.deleteCustomer(id);

	  if (result.changes === 0) {
		return ResponseBuilder.error('Failed to delete customer', 500, 'DELETE_FAILED');
	  }

	  return ResponseBuilder.success('Customer deleted successfully');
	}

	private async deleteAllCustomers(): Promise<Response> {
	  const result = await this.service.deleteAllCustomers();
	  return ResponseBuilder.success(`Deleted ${result.changes} customers successfully`);
	}

	private handleError(error: any): Response {
	  if (error instanceof AppError) {
		return ResponseBuilder.error(error.message, error.statusCode, error.code);
	  }

	  console.error('Unhandled error:', error);
	  return ResponseBuilder.error('Internal server error', 500, 'INTERNAL_ERROR');
	}
  }

  // 静态资源服务
  class StaticAssetHandler {
	private readonly htmlContent = `<!DOCTYPE html>
  <html lang="zh-CN" data-theme="light">
  <head>
	  <meta charset="UTF-8">
	  <meta name="viewport" content="width=device-width, initial-scale=1.0">
	  <title>D1 CRM - 现代化客户管理系统</title>
	  <script src="https://unpkg.com/[email protected]"></script>
	  <script src="https://unpkg.com/[email protected]"></script>
	  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
	  <style>
		  /* 这里包含之前提供的完整CSS样式 */
		  :root { --primary: #6366f1; --primary-dark: #4f46e5; /* ... 完整CSS样式 */ }
	  </style>
  </head>
  <body>
	  <div class="app-container" id="app">
		  <!-- 这里包含之前提供的完整HTML结构 -->
		  <header class="header">...</header>
		  <main class="main-content">...</main>
		  <div class="toast-container" id="toastContainer"></div>
	  </div>

	  <script>
		  // 这里包含之前提供的完整JavaScript代码
		  class D1CRM { /* ... 完整JavaScript代码 */ }
		  const app = new D1CRM();
	  </script>
  </body>
  </html>`;

	handleRequest(request: Request): Response {
	  const acceptHeader = request.headers.get('accept') || '';

	  if (acceptHeader.includes('text/html')) {
		return new Response(this.htmlContent, {
		  status: 200,
		  headers: {
			'Content-Type': 'text/html; charset=utf-8',
			'Cache-Control': 'no-cache',
		  },
		});
	  }

	  return ResponseBuilder.error('Not found', 404);
	}
  }

  // 主处理器
  export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
	  const url = new URL(request.url);
	  const pathname = url.pathname;

	  // 处理预检请求
	  if (request.method === 'OPTIONS') {
		return new Response(null, {
		  headers: {
			'Access-Control-Allow-Origin': '*',
			'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
			'Access-Control-Allow-Headers': 'Content-Type, Authorization',
			'Access-Control-Max-Age': '86400',
		  },
		});
	  }

	  // API 路由
	  if (pathname.startsWith('/api/customers')) {
		const customerHandler = new CustomerHandler(env);

		// 提取客户ID
		const idMatch = pathname.match(/^\/api\/customers\/(\d+)$/);
		const customerId = idMatch ? parseInt(idMatch[1]) : undefined;

		return customerHandler.handleRequest(request, pathname, customerId);
	  }

	  // 静态资源路由 (根路径)
	  if (pathname === '/' || pathname === '/index.html') {
		const staticHandler = new StaticAssetHandler();
		return staticHandler.handleRequest(request);
	  }

	  // 默认返回首页
	  const staticHandler = new StaticAssetHandler();
	  return staticHandler.handleRequest(request);
	},
  } satisfies ExportedHandler<Env>;