1.页面结构信息

2.页面部分的代码

kehu.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>电脑维修服务 | 客户信息登记系统</title>
    <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            background: linear-gradient(145deg, #e9eef3 0%, #dce3ec 100%);
            font-family: 'Inter', system-ui, sans-serif;
            padding: 2rem 1.5rem;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .service-card {
            max-width: 900px;
            width: 100%;
            background-color: #ffffff;
            border-radius: 2rem;
            box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.2);
            overflow: hidden;
        }
        .form-header {
            background: #0b2b3b;
            background-image: radial-gradient(circle at 10% 20%, #124c5f, #07212e);
            padding: 1.8rem 2rem;
            color: white;
        }
        .form-header h1 {
            font-size: 1.9rem;
            font-weight: 700;
            display: flex;
            align-items: center;
            gap: 0.75rem;
        }
        .form-header h1 i { font-size: 2rem; color: #7ac7e0; }
        .form-header p { margin-top: 0.6rem; font-size: 0.95rem; opacity: 0.85; }
        .form-body { padding: 2rem; }
        .input-group { margin-bottom: 1.6rem; display: flex; flex-direction: column; }
        .input-group label { font-weight: 600; font-size: 0.9rem; margin-bottom: 0.5rem; color: #1e2f3e; display: flex; align-items: center; gap: 0.5rem; }
        .required-star { color: #e03a3a; }
        .optional-tag { font-size: 0.7rem; background-color: #eef2f6; color: #5e6f8d; padding: 0.2rem 0.5rem; border-radius: 20px; margin-left: 8px; }
        input, textarea, select {
            width: 100%;
            padding: 0.85rem 1rem;
            font-size: 0.95rem;
            border: 1.5px solid #e2e8f0;
            border-radius: 1rem;
            transition: all 0.2s;
            outline: none;
        }
        input:focus, textarea:focus { border-color: #2c7da0; box-shadow: 0 0 0 3px rgba(44, 125, 160, 0.2); }
        input.error, textarea.error { border-color: #e03a3a !important; background-color: #fff6f5 !important; }
        textarea { resize: vertical; min-height: 100px; }
        .row-two { display: grid; grid-template-columns: 1fr 1fr; gap: 1.2rem; }
        .hint-text { font-size: 0.7rem; color: #5e6f8d; margin-top: 0.35rem; }
        .error-text { font-size: 0.7rem; color: #e03a3a; margin-top: 0.35rem; display: flex; align-items: center; gap: 4px; }
        .action-buttons { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 1rem; margin-top: 2rem; }
        .btn {
            border: none;
            padding: 0.85rem 1.6rem;
            font-weight: 600;
            font-size: 0.95rem;
            border-radius: 2rem;
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            gap: 0.6rem;
        }
        .btn-primary { background-color: #1f6e8c; color: white; }
        .btn-primary:hover { background-color: #0e5a75; }
        .btn-primary:disabled { background-color: #9bb7c4; cursor: not-allowed; }
        .btn-secondary { background-color: #eef2f6; border: 1px solid #cbd5e1; }
        .char-counter { font-size: 0.7rem; color: #7e8aa2; text-align: right; margin-top: 0.25rem; }
        hr { margin: 1rem 0 0.5rem; border: 0; height: 1px; background: linear-gradient(to right, #e2e8f0, transparent); }
        @media (max-width: 640px) {
            body { padding: 1rem; }
            .form-body { padding: 1.5rem; }
            .row-two { grid-template-columns: 1fr; gap: 1rem; }
            .action-buttons { flex-direction: column; }
            .btn { justify-content: center; }
        }
        .info-note {
            background: #f8fafc;
            border-radius: 1rem;
            padding: 0.8rem 1rem;
            margin-top: 1rem;
            font-size: 0.75rem;
            color: #4a627a;
            text-align: center;
            border: 1px dashed #cbd5e1;
        }
        .toast-msg {
            position: fixed;
            bottom: 2rem;
            left: 50%;
            transform: translateX(-50%);
            background-color: rgba(15, 35, 45, 0.95);
            color: white;
            padding: 0.9rem 1.6rem;
            border-radius: 3rem;
            display: flex;
            align-items: center;
            gap: 0.6rem;
            z-index: 1000;
            opacity: 0;
            visibility: hidden;
            transition: all 0.25s;
            pointer-events: none;
        }
        .toast-msg.show { opacity: 1; visibility: visible; }
        .toast-msg.success i { color: #6fcf97; }
        .toast-msg.error i { color: #e67e7e; }
    </style>
</head>
<body>
<div class="service-card">
    <div class="form-header">
        <h1><i class="fas fa-laptop-code"></i>电脑维修 · 客户信息登记</h1>
        <p>请填写以下信息,带 <span style="color:#ffaaa5;">*</span> 为必填项,工程师将尽快与您联系。</p>
    </div>
    <div class="form-body">
        <form id="repairForm">
            <div class="row-two">
                <div class="input-group">
                    <label><i class="fas fa-user"></i>客户姓名<span class="required-star">*</span></label>
                    <input type="text" id="customerName" name="customer_name" placeholder="例如:张明" maxlength="30">
                    <div class="hint-text">至少2个字符,最长30个字符</div>
                    <div id="nameError" class="error-text" style="display: none;"></div>
                </div>
                <div class="input-group">
                    <label><i class="fas fa-mobile-alt"></i>手机号码<span class="required-star">*</span></label>
                    <input type="tel" id="phone" name="phone" placeholder="11位手机号" maxlength="11">
                    <div class="hint-text">11位数字,以1开头</div>
                    <div id="phoneError" class="error-text" style="display: none;"></div>
                </div>
            </div>
            <div class="input-group">
                <label><i class="fas fa-desktop"></i>电脑型号<span class="required-star">*</span></label>
                <input type="text" id="deviceModel" name="device_model" placeholder="例:联想拯救者Y7000 / MacBook Pro 2021">
                <div id="modelError" class="error-text" style="display: none;"></div>
            </div>
            <div class="input-group">
                <label><i class="fas fa-clipboard-list"></i>故障描述<span class="required-star">*</span></label>
                <textarea id="faultDesc" name="fault_description" placeholder="请详细描述电脑出现的问题..." maxlength="1000"></textarea>
                <div class="char-counter"><span id="faultCharCount">0</span> / 1000 字符</div>
                <div class="hint-text">至少输入5个字符,越详细越好</div>
                <div id="faultError" class="error-text" style="display: none;"></div>
            </div>
            <div><label style="font-weight:600; color:#2c7da0;"><i class="fas fa-chevron-circle-down"></i>更多信息(可选)</label><hr></div>
            <div class="input-group">
                <label><i class="fas fa-barcode"></i>电脑SN/序列号<span class="optional-tag">可选</span></label>
                <input type="text" id="snCode" name="sn_code" placeholder="例: ABC123XYZ7890">
            </div>
            <div class="row-two">
                <div class="input-group">
                    <label><i class="fas fa-envelope"></i>电子邮箱<span class="optional-tag">可选</span></label>
                    <input type="email" id="email" name="customer_email" placeholder="example@domain.com">
                </div>
                <div class="input-group">
                    <label><i class="fab fa-weixin"></i>微信号<span class="optional-tag">可选</span></label>
                    <input type="text" id="wechat" name="customer_wechat" placeholder="微信号码">
                </div>
            </div>
            <div class="action-buttons">
                <button type="button" class="btn btn-secondary" id="resetBtn"><i class="fas fa-undo-alt"></i>重置表单</button>
                <button type="submit" class="btn btn-primary" id="submitBtn"><i class="fas fa-paper-plane"></i>提交维修申请</button>
            </div>
            <div class="info-note"><i class="fas fa-shield-alt"></i>信息仅用于维修服务,我们会严格保密。</div>
        </form>
    </div>
</div>
<div id="toastMsg" class="toast-msg"><i id="toastIcon" class="fas fa-check-circle"></i><span id="toastText">提示信息</span></div>
<script>
    const form = document.getElementById('repairForm');
    const nameInput = document.getElementById('customerName');
    const phoneInput = document.getElementById('phone');
    const modelInput = document.getElementById('deviceModel');
    const faultInput = document.getElementById('faultDesc');
    const resetBtn = document.getElementById('resetBtn');
    const submitBtn = document.getElementById('submitBtn');
    const toast = document.getElementById('toastMsg');
    const toastIcon = document.getElementById('toastIcon');
    const toastText = document.getElementById('toastText');
    const faultCharCountSpan = document.getElementById('faultCharCount');
    const nameErrorDiv = document.getElementById('nameError');
    const phoneErrorDiv = document.getElementById('phoneError');
    const modelErrorDiv = document.getElementById('modelError');
    const faultErrorDiv = document.getElementById('faultError');

    function showToast(message, isSuccess = true) {
        toastText.innerText = message;
        toastIcon.className = isSuccess ? 'fas fa-check-circle' : 'fas fa-exclamation-triangle';
        toast.className = `toast-msg ${isSuccess ? 'success' : 'error'} show`;
        setTimeout(() => toast.classList.remove('show'), 3500);
    }

    function resetFormFields() {
        form.reset();
        document.querySelectorAll('input, textarea').forEach(i => i.classList.remove('error'));
        [nameErrorDiv, phoneErrorDiv, modelErrorDiv, faultErrorDiv].forEach(div => { div.style.display = 'none'; div.innerHTML = ''; });
        updateFaultCharCount();
        showToast('表单已重置', true);
    }

    function updateFaultCharCount() { faultCharCountSpan.innerText = faultInput.value.length; }

    function validateNameField() {
        const val = nameInput.value.trim();
        if (!val) { showFieldError(nameInput, nameErrorDiv, '客户姓名不能为空'); return false; }
        if (val.length < 2) { showFieldError(nameInput, nameErrorDiv, '姓名至少需要2个字符'); return false; }
        clearFieldError(nameInput, nameErrorDiv);
        return true;
    }

    function validatePhoneField() {
        const val = phoneInput.value.trim();
        if (!val) { showFieldError(phoneInput, phoneErrorDiv, '手机号码不能为空'); return false; }
        if (!/^1[0-9]{10}$/.test(val)) { showFieldError(phoneInput, phoneErrorDiv, '手机号格式不正确(11位数字,以1开头)'); return false; }
        clearFieldError(phoneInput, phoneErrorDiv);
        return true;
    }

    function validateModelField() {
        const val = modelInput.value.trim();
        if (!val) { showFieldError(modelInput, modelErrorDiv, '电脑型号不能为空'); return false; }
        clearFieldError(modelInput, modelErrorDiv);
        return true;
    }

    function validateFaultField() {
        const val = faultInput.value.trim();
        if (!val) { showFieldError(faultInput, faultErrorDiv, '故障描述不能为空'); return false; }
        if (val.length < 5) { showFieldError(faultInput, faultErrorDiv, '故障描述至少需要5个字符'); return false; }
        clearFieldError(faultInput, faultErrorDiv);
        return true;
    }

    function showFieldError(field, errorDiv, message) {
        field.classList.add('error');
        errorDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${message}`;
        errorDiv.style.display = 'flex';
    }

    function clearFieldError(field, errorDiv) {
        field.classList.remove('error');
        errorDiv.style.display = 'none';
        errorDiv.innerHTML = '';
    }

    function validateAllFields() {
        return validateNameField() && validatePhoneField() && validateModelField() && validateFaultField();
    }

    nameInput.addEventListener('input', validateNameField);
    phoneInput.addEventListener('input', function() { this.value = this.value.replace(/[^\d]/g, '').slice(0, 11); validatePhoneField(); });
    modelInput.addEventListener('input', validateModelField);
    faultInput.addEventListener('input', function() { updateFaultCharCount(); validateFaultField(); });

    async function handleSubmit(e) {
        e.preventDefault();
        if (!validateAllFields()) { showToast('请正确填写所有必填项', false); return; }

        const formData = new FormData(form);
        const originalText = submitBtn.innerHTML;
        submitBtn.innerHTML = '<i class="fas fa-spinner fa-pulse"></i> 提交中...';
        submitBtn.disabled = true;

        try {
            const response = await fetch('/submit', { method: 'POST', body: formData });
            const result = await response.json();
            if (result.success) {
                showToast(result.message, true);
                setTimeout(resetFormFields, 1500);
            } else {
                showToast(result.message || '提交失败', false);
            }
        } catch (error) {
            console.error('提交错误:', error);
            showToast('网络连接失败,请检查网络后重试', false);
        } finally {
            submitBtn.innerHTML = originalText;
            submitBtn.disabled = false;
        }
    }

    form.addEventListener('submit', handleSubmit);
    resetBtn.addEventListener('click', (e) => { e.preventDefault(); resetFormFields(); });
    updateFaultCharCount();
</script>
</body>
</html>

维护客户的页面信息

2.2kehu-edit.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>客户数据管理 | 电脑维修系统</title>
    <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            background: linear-gradient(145deg, #e9eef3 0%, #dce3ec 100%);
            font-family: 'Inter', system-ui, sans-serif;
            padding: 2rem;
            min-height: 100vh;
        }
        .container {
            max-width: 1400px;
            margin: 0 auto;
        }
        /* 头部 */
        .header {
            background: white;
            border-radius: 1rem;
            padding: 1.5rem 2rem;
            margin-bottom: 1.5rem;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08);
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 1rem;
        }
        .header h1 {
            font-size: 1.6rem;
            color: #0b2b3b;
            display: flex;
            align-items: center;
            gap: 0.8rem;
        }
        .header h1 i { color: #1f6e8c; }
        .btn-add {
            background: #1f6e8c;
            color: white;
            border: none;
            padding: 0.7rem 1.5rem;
            border-radius: 2rem;
            cursor: pointer;
            font-weight: 600;
            display: flex;
            align-items: center;
            gap: 0.5rem;
            transition: all 0.2s;
        }
        .btn-add:hover { background: #0e5a75; transform: translateY(-1px); }
        /* 搜索和筛选栏 */
        .search-bar {
            background: white;
            border-radius: 1rem;
            padding: 1.2rem 1.5rem;
            margin-bottom: 1.5rem;
            display: flex;
            gap: 1rem;
            flex-wrap: wrap;
            align-items: flex-end;
        }
        .search-group {
            flex: 1;
            min-width: 180px;
        }
        .search-group label {
            display: block;
            font-size: 0.8rem;
            font-weight: 600;
            color: #4a627a;
            margin-bottom: 0.4rem;
        }
        .search-group input, .search-group select {
            width: 100%;
            padding: 0.6rem 1rem;
            border: 1.5px solid #e2e8f0;
            border-radius: 0.8rem;
            font-size: 0.9rem;
            outline: none;
        }
        .search-group input:focus { border-color: #1f6e8c; }
        .btn-search {
            background: #2c7da0;
            color: white;
            border: none;
            padding: 0.6rem 1.5rem;
            border-radius: 0.8rem;
            cursor: pointer;
            font-weight: 500;
        }
        .btn-reset {
            background: #eef2f6;
            border: 1px solid #cbd5e1;
            padding: 0.6rem 1.5rem;
            border-radius: 0.8rem;
            cursor: pointer;
        }
        /* 表格 */
        .table-container {
            background: white;
            border-radius: 1rem;
            overflow-x: auto;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08);
        }
        table {
            width: 100%;
            border-collapse: collapse;
        }
        th {
            background: #f8fafc;
            padding: 1rem;
            text-align: left;
            font-weight: 600;
            color: #1e2f3e;
            border-bottom: 2px solid #e2e8f0;
        }
        td {
            padding: 1rem;
            border-bottom: 1px solid #eef2f6;
            color: #334155;
        }
        tr:hover { background: #f8fafc; }
        .action-btns {
            display: flex;
            gap: 0.5rem;
        }
        .btn-icon {
            background: none;
            border: none;
            cursor: pointer;
            font-size: 1.1rem;
            padding: 0.3rem;
            border-radius: 0.4rem;
            transition: all 0.2s;
        }
        .btn-view { color: #2c7da0; }
        .btn-view:hover { background: #e0f2fe; }
        .btn-edit { color: #e67e22; }
        .btn-edit:hover { background: #fee2d6; }
        .btn-delete { color: #e03a3a; }
        .btn-delete:hover { background: #fee2e2; }
        /* 分页 */
        .pagination {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 1.5rem;
            background: white;
            border-radius: 1rem;
            margin-top: 1rem;
            flex-wrap: wrap;
            gap: 1rem;
        }
        .page-size {
            display: flex;
            align-items: center;
            gap: 0.5rem;
        }
        .page-size select {
            padding: 0.4rem;
            border-radius: 0.5rem;
            border: 1px solid #cbd5e1;
        }
        .page-buttons {
            display: flex;
            gap: 0.5rem;
            flex-wrap: wrap;
        }
        .page-btn {
            padding: 0.4rem 0.8rem;
            border: 1px solid #cbd5e1;
            background: white;
            border-radius: 0.5rem;
            cursor: pointer;
        }
        .page-btn.active {
            background: #1f6e8c;
            color: white;
            border-color: #1f6e8c;
        }
        .page-btn:hover:not(.active) { background: #eef2f6; }
        /* 模态框 */
        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            z-index: 1000;
            justify-content: center;
            align-items: center;
        }
        .modal-content {
            background: white;
            border-radius: 1.5rem;
            max-width: 800px;
            width: 90%;
            max-height: 85vh;
            overflow-y: auto;
            padding: 2rem;
        }
        .modal-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 1.5rem;
            padding-bottom: 1rem;
            border-bottom: 2px solid #e2e8f0;
        }
        .modal-header h2 { color: #0b2b3b; }
        .close-modal {
            font-size: 1.8rem;
            cursor: pointer;
            color: #94a3b8;
        }
        .detail-grid {
            display: grid;
            grid-template-columns: 1fr 2fr;
            gap: 1rem;
            margin-bottom: 1rem;
        }
        .detail-label {
            font-weight: 600;
            color: #475569;
        }
        .detail-value {
            color: #1e293b;
            word-break: break-all;
        }
        .form-group {
            margin-bottom: 1rem;
        }
        .form-group label {
            display: block;
            font-weight: 600;
            margin-bottom: 0.4rem;
            color: #334155;
        }
        .form-group input, .form-group textarea, .form-group select {
            width: 100%;
            padding: 0.6rem;
            border: 1.5px solid #e2e8f0;
            border-radius: 0.6rem;
        }
        .btn-save {
            background: #1f6e8c;
            color: white;
            padding: 0.7rem 1.5rem;
            border: none;
            border-radius: 0.6rem;
            cursor: pointer;
            margin-top: 1rem;
        }
        .toast-msg {
            position: fixed;
            bottom: 2rem;
            right: 2rem;
            background: #1f2f3e;
            color: white;
            padding: 0.8rem 1.5rem;
            border-radius: 2rem;
            z-index: 1100;
            opacity: 0;
            transition: opacity 0.3s;
        }
        .toast-msg.show { opacity: 1; }
        .badge {
            padding: 0.2rem 0.6rem;
            border-radius: 1rem;
            font-size: 0.7rem;
            font-weight: 600;
        }
        .badge-pending { background: #fef3c7; color: #d97706; }
        .badge-processing { background: #dbeafe; color: #2563eb; }
        .badge-done { background: #dcfce7; color: #16a34a; }
        .badge-cancelled { background: #fee2e2; color: #dc2626; }
        @media (max-width: 768px) {
            body { padding: 1rem; }
            .detail-grid { grid-template-columns: 1fr; }
        }
    </style>
</head>
<body>
<div class="container">
    <div class="header">
        <h1><i class="fas fa-chart-line"></i> 客户数据管理中心</h1>
        <button class="btn-add" onclick="openAddModal()"><i class="fas fa-plus"></i> 新增客户</button>
    </div>

    <div class="search-bar">
        <div class="search-group">
            <label>客户姓名</label>
            <input type="text" id="searchName" placeholder="请输入姓名">
        </div>
        <div class="search-group">
            <label>手机号</label>
            <input type="text" id="searchPhone" placeholder="手机号">
        </div>
        <div class="search-group">
            <label>维修状态</label>
            <select id="searchStatus">
                <option value="">全部</option>
                <option value="1">待处理</option>
                <option value="2">处理中</option>
                <option value="3">已完成</option>
                <option value="4">已取消</option>
                <option value="5">待回访</option>
            </select>
        </div>
        <button class="btn-search" onclick="loadData()"><i class="fas fa-search"></i> 搜索</button>
        <button class="btn-reset" onclick="resetSearch()"><i class="fas fa-redo"></i> 重置</button>
    </div>

    <div class="table-container">
        <table>
            <thead>
                <tr>
                    <th>ID</th><th>客户姓名</th><th>手机号</th><th>电脑型号</th>
                    <th>故障描述(前20字)</th><th>状态</th><th>提交时间</th><th>操作</th>
                </tr>
            </thead>
            <tbody id="tableBody">
                <tr><td colspan="8" style="text-align:center">加载中...</td></tr>
            </tbody>
        </table>
    </div>

    <div class="pagination">
        <div class="page-size">
            <span>每页显示</span>
            <select id="pageSize" onchange="changePageSize()">
                <option value="10">10</option><option value="20">20</option>
                <option value="30">30</option><option value="50">50</option>
            </select>
            <span>条</span>
        </div>
        <div class="page-buttons" id="pageButtons"></div>
        <div><span id="totalInfo"></span></div>
    </div>
</div>

<!-- 详情/编辑模态框 -->
<div id="detailModal" class="modal">
    <div class="modal-content">
        <div class="modal-header">
            <h2 id="modalTitle">客户详情</h2>
            <span class="close-modal" onclick="closeModal()">&times;</span>
        </div>
        <div id="modalBody"></div>
        <button class="btn-save" id="saveBtn" onclick="saveCustomer()">保存修改</button>
    </div>
</div>

<script>
    let currentPage = 1;
    let pageSize = 10;
    let totalPages = 1;
    let currentEditId = null;
    let allData = [];

    // 加载数据
    async function loadData() {
        const name = document.getElementById('searchName').value;
        const phone = document.getElementById('searchPhone').value;
        const status = document.getElementById('searchStatus').value;

        try {
            const response = await fetch(`/records?page=${currentPage}&size=${pageSize}&name=${encodeURIComponent(name)}&phone=${encodeURIComponent(phone)}&status=${status}`);
            const result = await response.json();
            if (result.success) {
                allData = result.data;
                renderTable(result.data);
                totalPages = result.totalPages || Math.ceil(result.total / pageSize);
                renderPagination();
                document.getElementById('totalInfo').innerHTML = `共 ${result.total || 0} 条记录`;
            } else {
                showToast('加载失败', false);
            }
        } catch (error) {
            showToast('网络错误', false);
        }
    }

    function renderTable(data) {
        const tbody = document.getElementById('tableBody');
        if (!data || data.length === 0) {
            tbody.innerHTML = '<tr><td colspan="8" style="text-align:center">暂无数据</td></tr>';
            return;
        }
        tbody.innerHTML = data.map(item => `
            <tr>
                <td>${item.id}</td>
                <td><strong>${escapeHtml(item.customer_name)}</strong></td>
                <td>${item.phone}</td>
                <td>${escapeHtml(item.device_model || '-')}</td>
                <td>${escapeHtml((item.fault_description || '').substring(0, 20))}${(item.fault_description || '').length > 20 ? '...' : ''}</td>
                <td>${getStatusBadge(item.status)}</td>
                <td>${item.submit_time ? new Date(item.submit_time).toLocaleString() : '-'}</td>
                <td class="action-btns">
                    <button class="btn-icon btn-view" onclick="viewDetail(${item.id})" title="查看详情"><i class="fas fa-eye"></i></button>
                    <button class="btn-icon btn-edit" onclick="editCustomer(${item.id})" title="编辑"><i class="fas fa-edit"></i></button>
                    <button class="btn-icon btn-delete" onclick="deleteCustomer(${item.id})" title="删除"><i class="fas fa-trash"></i></button>
                </td>
            </tr>
        `).join('');
    }

    function getStatusBadge(status) {
        const map = {1:'待处理',2:'处理中',3:'已完成',4:'已取消',5:'待回访'};
        const classMap = {1:'badge-pending',2:'badge-processing',3:'badge-done',4:'badge-cancelled',5:'badge-pending'};
        return `<span class="badge ${classMap[status]}">${map[status] || '未知'}</span>`;
    }

    function renderPagination() {
        const container = document.getElementById('pageButtons');
        if (totalPages <= 1) {
            container.innerHTML = '<button class="page-btn" disabled>1</button>';
            return;
        }
        let html = '';
        if (currentPage > 1) html += `<button class="page-btn" onclick="goPage(${currentPage-1})">上一页</button>`;
        for (let i = 1; i <= totalPages; i++) {
            if (i === 1 || i === totalPages || (i >= currentPage-2 && i <= currentPage+2)) {
                html += `<button class="page-btn ${i === currentPage ? 'active' : ''}" onclick="goPage(${i})">${i}</button>`;
            } else if (i === currentPage-3 || i === currentPage+3) {
                html += `<button class="page-btn" disabled>...</button>`;
            }
        }
        if (currentPage < totalPages) html += `<button class="page-btn" onclick="goPage(${currentPage+1})">下一页</button>`;
        container.innerHTML = html;
    }

    function goPage(page) { currentPage = page; loadData(); }
    function changePageSize() { pageSize = parseInt(document.getElementById('pageSize').value); currentPage = 1; loadData(); }
    function resetSearch() { document.getElementById('searchName').value = ''; document.getElementById('searchPhone').value = ''; document.getElementById('searchStatus').value = ''; currentPage = 1; loadData(); }

    // 查看详情
    async function viewDetail(id) {
        try {
            const response = await fetch(`/record/${id}`);
            const result = await response.json();
            if (result.success) {
                showDetailModal(result.data, 'view');
            }
        } catch (error) { showToast('加载失败', false); }
    }

    // 编辑客户
    async function editCustomer(id) {
        try {
            const response = await fetch(`/record/${id}`);
            const result = await response.json();
            if (result.success) {
                showDetailModal(result.data, 'edit');
            }
        } catch (error) { showToast('加载失败', false); }
    }

    // 新增客户
    function openAddModal() {
        currentEditId = null;
        showDetailModal({
            customer_name: '', phone: '', device_model: '', fault_description: '',
            sn_code: '', customer_email: '', customer_wechat: '', status: 1,
            emergency_level: '', source_channel: '', remark: ''
        }, 'edit');
    }

    function showDetailModal(data, mode) {
        const modal = document.getElementById('detailModal');
        const modalTitle = document.getElementById('modalTitle');
        const modalBody = document.getElementById('modalBody');
        const saveBtn = document.getElementById('saveBtn');

        if (mode === 'view') {
            modalTitle.innerText = '客户详细信息';
            saveBtn.style.display = 'none';
            modalBody.innerHTML = `
                <div class="detail-grid">
                    <div class="detail-label">ID:</div><div class="detail-value">${data.id}</div>
                    <div class="detail-label">客户姓名:</div><div class="detail-value">${escapeHtml(data.customer_name)}</div>
                    <div class="detail-label">手机号:</div><div class="detail-value">${data.phone}</div>
                    <div class="detail-label">电脑型号:</div><div class="detail-value">${escapeHtml(data.device_model || '-')}</div>
                    <div class="detail-label">电脑SN:</div><div class="detail-value">${escapeHtml(data.sn_code || '-')}</div>
                    <div class="detail-label">故障描述:</div><div class="detail-value">${escapeHtml(data.fault_description || '-')}</div>
                    <div class="detail-label">电子邮箱:</div><div class="detail-value">${escapeHtml(data.customer_email || '-')}</div>
                    <div class="detail-label">微信号:</div><div class="detail-value">${escapeHtml(data.customer_wechat || '-')}</div>
                    <div class="detail-label">紧急程度:</div><div class="detail-value">${getEmergencyText(data.emergency_level)}</div>
                    <div class="detail-label">维修状态:</div><div class="detail-value">${getStatusBadge(data.status)}</div>
                    <div class="detail-label">来源渠道:</div><div class="detail-value">${escapeHtml(data.source_channel || '-')}</div>
                    <div class="detail-label">指派工程师:</div><div class="detail-value">${escapeHtml(data.assign_engineer || '-')}</div>
                    <div class="detail-label">维修费用:</div><div class="detail-value">${data.repair_cost ? '¥' + data.repair_cost : '-'}</div>
                    <div class="detail-label">维修结果:</div><div class="detail-value">${escapeHtml(data.repair_result || '-')}</div>
                    <div class="detail-label">备注:</div><div class="detail-value">${escapeHtml(data.remark || '-')}</div>
                    <div class="detail-label">提交时间:</div><div class="detail-value">${data.submit_time ? new Date(data.submit_time).toLocaleString() : '-'}</div>
                    <div class="detail-label">更新时间:</div><div class="detail-value">${data.update_time ? new Date(data.update_time).toLocaleString() : '-'}</div>
                </div>
            `;
        } else {
            modalTitle.innerText = currentEditId ? '编辑客户信息' : '新增客户';
            saveBtn.style.display = 'block';
            currentEditId = data.id || null;
            modalBody.innerHTML = `
                <div class="form-group"><label>客户姓名 *</label><input type="text" id="edit_name" value="${escapeHtml(data.customer_name || '')}"></div>
                <div class="form-group"><label>手机号 *</label><input type="text" id="edit_phone" value="${data.phone || ''}"></div>
                <div class="form-group"><label>电脑型号 *</label><input type="text" id="edit_model" value="${escapeHtml(data.device_model || '')}"></div>
                <div class="form-group"><label>故障描述 *</label><textarea id="edit_fault" rows="3">${escapeHtml(data.fault_description || '')}</textarea></div>
                <div class="form-group"><label>电脑SN</label><input type="text" id="edit_sn" value="${escapeHtml(data.sn_code || '')}"></div>
                <div class="form-group"><label>电子邮箱</label><input type="email" id="edit_email" value="${escapeHtml(data.customer_email || '')}"></div>
                <div class="form-group"><label>微信号</label><input type="text" id="edit_wechat" value="${escapeHtml(data.customer_wechat || '')}"></div>
                <div class="form-group"><label>维修状态</label><select id="edit_status">
                    <option value="1" ${data.status == 1 ? 'selected' : ''}>待处理</option>
                    <option value="2" ${data.status == 2 ? 'selected' : ''}>处理中</option>
                    <option value="3" ${data.status == 3 ? 'selected' : ''}>已完成</option>
                    <option value="4" ${data.status == 4 ? 'selected' : ''}>已取消</option>
                    <option value="5" ${data.status == 5 ? 'selected' : ''}>待回访</option>
                </select></div>
                <div class="form-group"><label>紧急程度</label><select id="edit_emergency">
                    <option value="">请选择</option>
                    <option value="1" ${data.emergency_level == 1 ? 'selected' : ''}>低</option>
                    <option value="2" ${data.emergency_level == 2 ? 'selected' : ''}>中</option>
                    <option value="3" ${data.emergency_level == 3 ? 'selected' : ''}>高</option>
                    <option value="4" ${data.emergency_level == 4 ? 'selected' : ''}>紧急</option>
                </select></div>
                <div class="form-group"><label>来源渠道</label><input type="text" id="edit_source" value="${escapeHtml(data.source_channel || '')}"></div>
                <div class="form-group"><label>指派工程师</label><input type="text" id="edit_engineer" value="${escapeHtml(data.assign_engineer || '')}"></div>
                <div class="form-group"><label>维修费用</label><input type="number" id="edit_cost" step="0.01" value="${data.repair_cost || ''}"></div>
                <div class="form-group"><label>维修结果</label><textarea id="edit_result" rows="2">${escapeHtml(data.repair_result || '')}</textarea></div>
                <div class="form-group"><label>备注</label><textarea id="edit_remark" rows="2">${escapeHtml(data.remark || '')}</textarea></div>
            `;
        }
        modal.style.display = 'flex';
    }

    async function saveCustomer() {
        const formData = {
            customer_name: document.getElementById('edit_name').value.trim(),
            phone: document.getElementById('edit_phone').value.trim(),
            device_model: document.getElementById('edit_model').value.trim(),
            fault_description: document.getElementById('edit_fault').value.trim(),
            sn_code: document.getElementById('edit_sn').value || null,
            customer_email: document.getElementById('edit_email').value || null,
            customer_wechat: document.getElementById('edit_wechat').value || null,
            status: parseInt(document.getElementById('edit_status').value),
            emergency_level: document.getElementById('edit_emergency').value || null,
            source_channel: document.getElementById('edit_source').value || null,
            assign_engineer: document.getElementById('edit_engineer').value || null,
            repair_cost: document.getElementById('edit_cost').value || null,
            repair_result: document.getElementById('edit_result').value || null,
            remark: document.getElementById('edit_remark').value || null
        };

        if (!formData.customer_name || !formData.phone || !formData.device_model || !formData.fault_description) {
            showToast('请填写所有必填项', false); return;
        }
        if (!/^1[0-9]{10}$/.test(formData.phone)) { showToast('手机号格式不正确', false); return; }

        const url = currentEditId ? `/record/${currentEditId}` : '/record';
        const method = currentEditId ? 'PUT' : 'POST';

        try {
            const response = await fetch(url, { method, headers: {'Content-Type':'application/json'}, body: JSON.stringify(formData) });
            const result = await response.json();
            if (result.success) {
                showToast(result.message, true);
                closeModal();
                loadData();
            } else { showToast(result.message || '操作失败', false); }
        } catch (error) { showToast('网络错误', false); }
    }

    async function deleteCustomer(id) {
        if (!confirm('确定要删除这条记录吗?')) return;
        try {
            const response = await fetch(`/record/${id}`, { method: 'DELETE' });
            const result = await response.json();
            if (result.success) { showToast('删除成功', true); loadData(); }
            else { showToast('删除失败', false); }
        } catch (error) { showToast('网络错误', false); }
    }

    function closeModal() { document.getElementById('detailModal').style.display = 'none'; }
    function showToast(msg, success) {
        const toast = document.createElement('div');
        toast.className = 'toast-msg';
        toast.innerHTML = `<i class="fas ${success ? 'fa-check-circle' : 'fa-exclamation-circle'}"></i> ${msg}`;
        document.body.appendChild(toast);
        setTimeout(() => toast.classList.add('show'), 10);
        setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000);
    }
    function getEmergencyText(level) {
        const map = {1:'低',2:'中',3:'高',4:'紧急'};
        return map[level] || '-';
    }
    function escapeHtml(str) { if(!str) return ''; return str.replace(/[&<>]/g, function(m){if(m==='&') return '&amp;'; if(m==='<') return '&lt;'; if(m==='>') return '&gt;'; return m;}); }

    loadData();
</script>
</body>
</html>

3.主体功能部分的代码 kehu.py

# kehu.py
import mysql.connector
from mysql.connector import Error
import re
from flask import Flask, request, jsonify, render_template

app = Flask(__name__)

# 数据库配置
DB_CONFIG = {
    'host': 'localhost',
    'port': 3306,
    'user': 'root',
    'password': 'root',
    'database': 'test',
    'charset': 'utf8'
}


class KehuDB:
    """客户信息数据库操作类"""

    def __init__(self):
        self.conn = None
        self.cursor = None

    def connect(self):
        try:
            self.conn = mysql.connector.connect(**DB_CONFIG)
            self.cursor = self.conn.cursor(dictionary=True)
            return True
        except Error as e:
            print(f"数据库连接失败: {e}")
            return False

    def close(self):
        if self.cursor:
            self.cursor.close()
        if self.conn:
            self.conn.close()

    def insert_customer(self, data):
        if not self.connect():
            return False, "数据库连接失败", None

        try:
            sql = """
                INSERT INTO kehu (
                    customer_name, phone, device_model, fault_description,
                    sn_code, customer_email, customer_qq, customer_wechat,
                    status, submit_time, update_time
                ) VALUES (
                    %(customer_name)s, %(phone)s, %(device_model)s, %(fault_description)s,
                    %(sn_code)s, %(customer_email)s, %(customer_qq)s, %(customer_wechat)s,
                    1, NOW(), NOW()
                )
            """
            self.cursor.execute(sql, data)
            self.conn.commit()
            insert_id = self.cursor.lastrowid
            return True, "数据插入成功", insert_id
        except Error as e:
            self.conn.rollback()
            print(f"数据库错误详情: {e}")
            return False, f"数据库错误: {e}", None
        except Exception as e:
            self.conn.rollback()
            print(f"错误详情: {e}")
            return False, f"错误: {e}", None
        finally:
            self.close()

    def get_all_records(self, limit=100):
        if not self.connect():
            return []
        try:
            sql = "SELECT * FROM kehu ORDER BY submit_time DESC LIMIT %s"
            self.cursor.execute(sql, (limit,))
            return self.cursor.fetchall()
        except Error as e:
            print(f"查询失败: {e}")
            return []
        finally:
            self.close()

    def get_record_by_id(self, record_id):
        if not self.connect():
            return None
        try:
            sql = "SELECT * FROM kehu WHERE id = %s"
            self.cursor.execute(sql, (record_id,))
            return self.cursor.fetchone()
        except Error as e:
            print(f"查询失败: {e}")
            return None
        finally:
            self.close()

    def get_records_by_phone(self, phone):
        if not self.connect():
            return []
        try:
            sql = "SELECT * FROM kehu WHERE phone = %s ORDER BY submit_time DESC"
            self.cursor.execute(sql, (phone,))
            return self.cursor.fetchall()
        except Error as e:
            print(f"查询失败: {e}")
            return []
        finally:
            self.close()

    def update_status(self, record_id, status):
        if not self.connect():
            return False
        try:
            sql = "UPDATE kehu SET status = %s, update_time = NOW() WHERE id = %s"
            self.cursor.execute(sql, (status, record_id))
            self.conn.commit()
            return True
        except Error as e:
            print(f"更新失败: {e}")
            self.conn.rollback()
            return False
        finally:
            self.close()

    # ========== 新增方法 ==========
    def get_records_paginated(self, page, size, name, phone, status):
        """分页查询记录(支持搜索)"""
        if not self.connect():
            return [], 0

        try:
            # 构建查询条件
            conditions = []
            params = []
            if name:
                conditions.append("customer_name LIKE %s")
                params.append(f"%{name}%")
            if phone:
                conditions.append("phone LIKE %s")
                params.append(f"%{phone}%")
            if status:
                conditions.append("status = %s")
                params.append(status)

            where_clause = " WHERE " + " AND ".join(conditions) if conditions else ""

            # 查询总数
            count_sql = f"SELECT COUNT(*) as total FROM kehu{where_clause}"
            self.cursor.execute(count_sql, params)
            total = self.cursor.fetchone()['total']

            # 查询分页数据
            offset = (page - 1) * size
            sql = f"SELECT * FROM kehu{where_clause} ORDER BY submit_time DESC LIMIT %s OFFSET %s"
            self.cursor.execute(sql, params + [size, offset])
            records = self.cursor.fetchall()

            return records, total
        except Error as e:
            print(f"查询失败: {e}")
            return [], 0
        finally:
            self.close()

    def create_customer(self, data):
        """新增客户(完整版)"""
        if not self.connect():
            return False, "数据库连接失败", None

        try:
            sql = """
                INSERT INTO kehu (
                    customer_name, phone, device_model, fault_description,
                    sn_code, customer_email, customer_qq, customer_wechat,
                    emergency_level, source_channel, assign_engineer,
                    repair_cost, repair_result, remark, status, submit_time, update_time
                ) VALUES (
                    %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()
                )
            """
            values = (
                data.get('customer_name'), data.get('phone'), data.get('device_model'), data.get('fault_description'),
                data.get('sn_code'), data.get('customer_email'), data.get('customer_qq'), data.get('customer_wechat'),
                data.get('emergency_level'), data.get('source_channel'), data.get('assign_engineer'),
                data.get('repair_cost'), data.get('repair_result'), data.get('remark'),
                data.get('status', 1)
            )
            self.cursor.execute(sql, values)
            self.conn.commit()
            insert_id = self.cursor.lastrowid
            return True, "添加成功", insert_id
        except Error as e:
            self.conn.rollback()
            print(f"数据库错误详情: {e}")
            return False, f"数据库错误: {e}", None
        except Exception as e:
            self.conn.rollback()
            print(f"错误详情: {e}")
            return False, f"错误: {e}", None
        finally:
            self.close()

    def update_customer(self, record_id, data):
        """更新客户(完整版)"""
        if not self.connect():
            return False, "数据库连接失败"

        try:
            sql = """
                UPDATE kehu SET
                    customer_name=%s, phone=%s, device_model=%s, fault_description=%s,
                    sn_code=%s, customer_email=%s, customer_qq=%s, customer_wechat=%s,
                    status=%s, emergency_level=%s, source_channel=%s, assign_engineer=%s,
                    repair_cost=%s, repair_result=%s, remark=%s, update_time=NOW()
                WHERE id=%s
            """
            values = (
                data.get('customer_name'), data.get('phone'), data.get('device_model'), data.get('fault_description'),
                data.get('sn_code'), data.get('customer_email'), data.get('customer_qq'), data.get('customer_wechat'),
                data.get('status', 1), data.get('emergency_level'), data.get('source_channel'),
                data.get('assign_engineer'),
                data.get('repair_cost'), data.get('repair_result'), data.get('remark'), record_id
            )
            self.cursor.execute(sql, values)
            self.conn.commit()
            return True, "更新成功"
        except Error as e:
            self.conn.rollback()
            print(f"更新失败: {e}")
            return False, f"数据库错误: {e}"
        finally:
            self.close()

    def delete_customer(self, record_id):
        """删除客户"""
        if not self.connect():
            return False, "数据库连接失败"
        try:
            sql = "DELETE FROM kehu WHERE id = %s"
            self.cursor.execute(sql, (record_id,))
            self.conn.commit()
            return True, "删除成功"
        except Error as e:
            self.conn.rollback()
            print(f"删除失败: {e}")
            return False, f"删除失败: {e}"
        finally:
            self.close()


def validate_form_data(data):
    errors = {}

    name = data.get('customer_name', '').strip()
    if not name:
        errors['customer_name'] = '客户姓名不能为空'
    elif len(name) < 2 or len(name) > 30:
        errors['customer_name'] = '姓名长度应为2-30个字符'

    phone = data.get('phone', '').strip()
    phone_pattern = re.compile(r'^1[0-9]{10}$')
    if not phone:
        errors['phone'] = '手机号码不能为空'
    elif not phone_pattern.match(phone):
        errors['phone'] = '手机号格式不正确(11位数字,以1开头)'

    model = data.get('device_model', '').strip()
    if not model:
        errors['device_model'] = '电脑型号不能为空'

    fault = data.get('fault_description', '').strip()
    if not fault:
        errors['fault_description'] = '故障描述不能为空'
    elif len(fault) < 5:
        errors['fault_description'] = '故障描述至少需要5个字符'

    return errors


# ======================================================
# Flask 路由定义
# ======================================================

@app.route('/')
def index():
    """显示表单页面"""
    return render_template('kehu.html')


@app.route('/edit')
def edit_page():
    """客户管理页面"""
    return render_template('kehu-edit.html')


@app.route('/submit', methods=['POST'])
def submit():
    """处理表单提交"""
    print("收到提交请求")
    print("表单数据:", request.form)

    form_data = {
        'customer_name': request.form.get('customer_name', '').strip(),
        'phone': request.form.get('phone', '').strip(),
        'device_model': request.form.get('device_model', '').strip(),
        'fault_description': request.form.get('fault_description', '').strip(),
        'sn_code': request.form.get('sn_code') or None,
        'customer_email': request.form.get('customer_email') or None,
        'customer_qq': request.form.get('customer_qq') or None,
        'customer_wechat': request.form.get('customer_wechat') or None,
    }

    errors = validate_form_data(form_data)
    if errors:
        return jsonify({'success': False, 'errors': errors, 'message': '验证失败'}), 400

    db = KehuDB()
    success, message, insert_id = db.insert_customer(form_data)

    if success:
        return jsonify({'success': True, 'message': '维修申请提交成功!', 'id': insert_id})
    else:
        return jsonify({'success': False, 'message': message}), 500


@app.route('/records', methods=['GET'])
def get_records():
    """获取分页记录(支持搜索)"""
    page = int(request.args.get('page', 1))
    size = int(request.args.get('size', 10))
    name = request.args.get('name', '')
    phone = request.args.get('phone', '')
    status = request.args.get('status', '')

    db = KehuDB()
    records, total = db.get_records_paginated(page, size, name, phone, status)

    return jsonify({
        'success': True,
        'data': records,
        'total': total,
        'page': page,
        'size': size,
        'totalPages': (total + size - 1) // size
    })


@app.route('/record/<int:record_id>', methods=['GET'])
def get_record(record_id):
    db = KehuDB()
    record = db.get_record_by_id(record_id)
    if record:
        return jsonify({'success': True, 'data': record})
    return jsonify({'success': False, 'message': '记录不存在'}), 404


@app.route('/record', methods=['POST'])
def create_record():
    """新增客户"""
    data = request.get_json()

    # 验证必填字段
    if not data.get('customer_name') or not data.get('phone') or not data.get('device_model') or not data.get(
            'fault_description'):
        return jsonify({'success': False, 'message': '请填写所有必填项'}), 400

    if not re.match(r'^1[0-9]{10}$', data.get('phone', '')):
        return jsonify({'success': False, 'message': '手机号格式不正确'}), 400

    db = KehuDB()
    success, message, insert_id = db.create_customer(data)

    if success:
        return jsonify({'success': True, 'message': message, 'id': insert_id})
    return jsonify({'success': False, 'message': message}), 500


@app.route('/record/<int:record_id>', methods=['PUT'])
def update_record(record_id):
    """更新客户(完整版)"""
    data = request.get_json()

    # 如果只是更新状态(兼容旧接口)
    if 'status' in data and len(data) == 1:
        status = data.get('status')
        if not status or status not in [1, 2, 3, 4, 5]:
            return jsonify({'success': False, 'message': '无效的状态值'}), 400
        db = KehuDB()
        success = db.update_status(record_id, status)
        if success:
            return jsonify({'success': True, 'message': '状态更新成功'})
        return jsonify({'success': False, 'message': '更新失败'}), 500

    # 完整更新
    db = KehuDB()
    success, message = db.update_customer(record_id, data)

    if success:
        return jsonify({'success': True, 'message': message})
    return jsonify({'success': False, 'message': message}), 500


@app.route('/record/<int:record_id>', methods=['DELETE'])
def delete_record(record_id):
    """删除记录"""
    db = KehuDB()
    success, message = db.delete_customer(record_id)

    if success:
        return jsonify({'success': True, 'message': message})
    return jsonify({'success': False, 'message': message}), 500


@app.route('/search', methods=['GET'])
def search_by_phone():
    phone = request.args.get('phone', '')
    if not phone:
        return jsonify({'success': False, 'message': '请提供手机号'}), 400

    db = KehuDB()
    records = db.get_records_by_phone(phone)
    return jsonify({'success': True, 'data': records})


@app.route('/test-db')
def test_db():
    """测试数据库连接"""
    db = KehuDB()
    if db.connect():
        db.close()
        return jsonify({'success': True, 'message': '数据库连接成功'})
    return jsonify({'success': False, 'message': '数据库连接失败'}), 500


if __name__ == '__main__':
    print("=" * 50)
    print("启动服务器...")
    print("访问地址: http://localhost:5000")
    print("客户登记: http://localhost:5000")
    print("客户管理: http://localhost:5000/edit")
    print("测试数据库: http://localhost:5000/test-db")
    print("=" * 50)
    app.run(debug=True, host='0.0.0.0', port=5000)

上述部分就可以运行

-- ======================================================
-- 表名: kehu (客户维修信息表)
-- 描述: 存储电脑维修客户的个人信息及设备故障记录
-- 必填字段: 客户姓名、手机号、电脑型号、故障描述
-- 作者: 自动生成
-- 日期: 2026-04-19
-- ======================================================

DROP TABLE IF EXISTS `kehu`;
CREATE TABLE `kehu` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID,主键,自增',
    `customer_name` VARCHAR(50) NOT NULL COMMENT '客户姓名,必填,2-30个字符',
    `phone` CHAR(11) NOT NULL COMMENT '手机号码,必填,11位数字,格式:1开头的11位数字',
    `device_model` VARCHAR(100) NOT NULL COMMENT '电脑型号,必填,如:联想拯救者Y7000、MacBook Pro 2021',
    `fault_description` TEXT NOT NULL COMMENT '故障描述,必填,详细说明电脑问题,至少5个字符',
    `sn_code` VARCHAR(50) DEFAULT NULL COMMENT '电脑序列号(SN),可选,设备唯一标识',
    `customer_email` VARCHAR(100) DEFAULT NULL COMMENT '电子邮箱,可选,用于发送维修进度',
    `customer_qq` VARCHAR(20) DEFAULT NULL COMMENT 'QQ号码,可选,备用联系方式',
    `customer_wechat` VARCHAR(50) DEFAULT NULL COMMENT '微信号,可选,便于沟通',
    `appointment_time` DATETIME DEFAULT NULL COMMENT '预约上门时间,可选',
    `device_accessories` VARCHAR(200) DEFAULT NULL COMMENT '附带配件,可选,如:电源适配器、鼠标等',
    `purchase_date` DATE DEFAULT NULL COMMENT '购买日期,可选,用于判断是否在保修期',
    `is_under_warranty` TINYINT(1) DEFAULT NULL COMMENT '是否在保修期内,可选,0-否,1-是,NULL-未知',
    `emergency_level` TINYINT UNSIGNED DEFAULT NULL COMMENT '紧急程度,可选,1-低,2-中,3-高,4-紧急',
    `source_channel` VARCHAR(50) DEFAULT NULL COMMENT '来源渠道,可选,如:网站、门店、电话推荐',
    `status` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '维修状态:1-待处理,2-处理中,3-已完成,4-已取消,5-待回访',
    `submit_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间,记录客户提交申请的时间',
    `update_time` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '更新时间,记录最后修改时间',
    `assign_engineer` VARCHAR(50) DEFAULT NULL COMMENT '指派工程师姓名,可选',
    `repair_cost` DECIMAL(10,2) DEFAULT NULL COMMENT '维修费用,可选,单位:元,精确到分',
    `repair_result` VARCHAR(500) DEFAULT NULL COMMENT '维修结果说明,可选',
    `remark` VARCHAR(500) DEFAULT NULL COMMENT '内部备注,可选,员工可记录额外信息',
    PRIMARY KEY (`id`),
    KEY `idx_phone` (`phone`),
    KEY `idx_customer_name` (`customer_name`),
    KEY `idx_status` (`status`),
    KEY `idx_submit_time` (`submit_time`),
    KEY `idx_device_model` (`device_model`),
    KEY `idx_sn_code` (`sn_code`),
    KEY `idx_appointment_time` (`appointment_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='电脑维修客户信息表';


-- ======================================================
-- 插入测试数据(示例)
-- ======================================================

-- 示例1:完整填写所有字段
INSERT INTO `kehu` (
    `customer_name`,
    `phone`,
    `device_model`,
    `fault_description`,
    `sn_code`,
    `customer_email`,
    `customer_qq`,
    `customer_wechat`,
    `appointment_time`,
    `device_accessories`,
    `purchase_date`,
    `is_under_warranty`,
    `emergency_level`,
    `source_channel`,
    `status`
) VALUES (
    '张明',
    '13812345678',
    '联想 拯救者Y7000',
    '电脑无法开机,按电源键无任何反应,电源适配器指示灯正常,已尝试更换插座无效',
    'ABC123XYZ7890',
    'zhangming@example.com',
    '123456789',
    'zm_wx123',
    '2026-04-20 14:00:00',
    '电源适配器、鼠标',
    '2025-01-15',
    1,  -- 在保修期内
    3,  -- 紧急程度:高
    '官方网站',
    1   -- 待处理
);

-- 示例2:仅填写必填字段(客户姓名、手机号、电脑型号、故障描述)
INSERT INTO `kehu` (
    `customer_name`,
    `phone`,
    `device_model`,
    `fault_description`
) VALUES (
    '李芳',
    '15987654321',
    'MacBook Pro 2021 14寸',
    '屏幕出现闪烁条纹,外接显示器正常,疑似排线问题,需要检测'
);

-- 示例3:另一个完整示例
INSERT INTO `kehu` (
    `customer_name`,
    `phone`,
    `device_model`,
    `fault_description`,
    `sn_code`,
    `emergency_level`,
    `status`
) VALUES (
    '王伟',
    '18655556666',
    '戴尔 XPS 15 9520',
    '频繁蓝屏,错误代码0x0000007B,已重装系统仍出现,怀疑硬盘故障',
    'DELL2026SN001',
    2,  -- 紧急程度:中
    2   -- 处理中
);

-- ======================================================
-- 常用查询示例
-- ======================================================

-- 1. 查询今日新增的维修申请(仅显示必填字段)
SELECT `id`, `customer_name`, `phone`, `device_model`, `fault_description`, `submit_time`
FROM `kehu`
WHERE DATE(`submit_time`) = CURDATE()
ORDER BY `submit_time` DESC;

-- 2. 按手机号查询客户历史维修记录
SELECT * FROM `kehu` WHERE `phone` = '13812345678' ORDER BY `submit_time` DESC;

-- 3. 统计各电脑型号的故障数量
SELECT `device_model`, COUNT(*) as fault_count
FROM `kehu`
GROUP BY `device_model`
ORDER BY fault_count DESC;

-- 4. 查询紧急程度高的待处理工单
SELECT * FROM `kehu`
WHERE `status` = 1 AND `emergency_level` >= 3
ORDER BY `emergency_level` DESC, `submit_time` ASC;

-- ======================================================
-- 说明文档
-- ======================================================
/*
【必填字段说明】(NOT NULL)
1. customer_name      - 客户姓名,长度2-30字符
2. phone              - 手机号,11位,格式校验:1开头
3. device_model       - 电脑型号,自由文本
4. fault_description  - 故障描述,至少5个字符

【可选字段说明】(DEFAULT NULL)
1. sn_code            - 序列号(建议填写,便于保修查询)
2. customer_email     - 电子邮箱
3. customer_qq        - QQ号
4. customer_wechat    - 微信号
5. appointment_time   - 预约时间
6. device_accessories - 附带配件
7. purchase_date      - 购买日期
8. is_under_warranty  - 是否在保修期(0-否,1-是)
9. emergency_level    - 紧急程度(1-低,2-中,3-高,4-紧急)
10. source_channel    - 来源渠道

【状态枚举】
1 - 待处理
2 - 处理中
3 - 已完成
4 - 已取消
5 - 待回访

【建表后验证】
-- 查看表结构
DESC kehu;

-- 查看约束
SHOW CREATE TABLE kehu;

-- 测试必填字段(以下插入应失败,因为缺少必填字段)
-- INSERT INTO `kehu` (`customer_name`) VALUES ('测试');  -- 会报错
*/

truncate table test.kehu;

上述代码 时间上有点问题

建议先修改后端代码,因为问题根源是数据库返回的时间格式不正确。

kehu.py

# kehu.py
import mysql.connector
from mysql.connector import Error
import re
from flask import Flask, request, jsonify, render_template

app = Flask(__name__)

# 数据库配置
DB_CONFIG = {
    'host': 'localhost',
    'port': 3306,
    'user': 'root',
    'password': 'root',
    'database': 'test',
    'charset': 'utf8'
}


class KehuDB:
    """客户信息数据库操作类"""

    def __init__(self):
        self.conn = None
        self.cursor = None

    def connect(self):
        try:
            self.conn = mysql.connector.connect(**DB_CONFIG)
            self.cursor = self.conn.cursor(dictionary=True)
            return True
        except Error as e:
            print(f"数据库连接失败: {e}")
            return False

    def close(self):
        if self.cursor:
            self.cursor.close()
        if self.conn:
            self.conn.close()

    def insert_customer(self, data):
        if not self.connect():
            return False, "数据库连接失败", None

        try:
            sql = """
                INSERT INTO kehu (
                    customer_name, phone, device_model, fault_description,
                    sn_code, customer_email, customer_qq, customer_wechat,
                    status, submit_time, update_time
                ) VALUES (
                    %(customer_name)s, %(phone)s, %(device_model)s, %(fault_description)s,
                    %(sn_code)s, %(customer_email)s, %(customer_qq)s, %(customer_wechat)s,
                    1, NOW(), NOW()
                )
            """
            self.cursor.execute(sql, data)
            self.conn.commit()
            insert_id = self.cursor.lastrowid
            return True, "数据插入成功", insert_id
        except Error as e:
            self.conn.rollback()
            print(f"数据库错误详情: {e}")
            return False, f"数据库错误: {e}", None
        except Exception as e:
            self.conn.rollback()
            print(f"错误详情: {e}")
            return False, f"错误: {e}", None
        finally:
            self.close()

    def get_all_records(self, limit=100):
        if not self.connect():
            return []
        try:
            sql = "SELECT * FROM kehu ORDER BY submit_time DESC LIMIT %s"
            self.cursor.execute(sql, (limit,))
            return self.cursor.fetchall()
        except Error as e:
            print(f"查询失败: {e}")
            return []
        finally:
            self.close()

    def get_record_by_id(self, record_id):
        if not self.connect():
            return None
        try:
            sql = "SELECT * FROM kehu WHERE id = %s"
            self.cursor.execute(sql, (record_id,))
            return self.cursor.fetchone()
        except Error as e:
            print(f"查询失败: {e}")
            return None
        finally:
            self.close()

    def get_records_by_phone(self, phone):
        if not self.connect():
            return []
        try:
            sql = "SELECT * FROM kehu WHERE phone = %s ORDER BY submit_time DESC"
            self.cursor.execute(sql, (phone,))
            return self.cursor.fetchall()
        except Error as e:
            print(f"查询失败: {e}")
            return []
        finally:
            self.close()

    def update_status(self, record_id, status):
        if not self.connect():
            return False
        try:
            sql = "UPDATE kehu SET status = %s, update_time = NOW() WHERE id = %s"
            self.cursor.execute(sql, (status, record_id))
            self.conn.commit()
            return True
        except Error as e:
            print(f"更新失败: {e}")
            self.conn.rollback()
            return False
        finally:
            self.close()

    # ========== 新增方法 ==========
    def get_records_paginated(self, page, size, name, phone, status):
        """分页查询记录(支持搜索)"""
        if not self.connect():
            return [], 0

        try:
            # 构建查询条件
            conditions = []
            params = []
            if name:
                conditions.append("customer_name LIKE %s")
                params.append(f"%{name}%")
            if phone:
                conditions.append("phone LIKE %s")
                params.append(f"%{phone}%")
            if status:
                conditions.append("status = %s")
                params.append(status)

            where_clause = " WHERE " + " AND ".join(conditions) if conditions else ""

            # 查询总数
            count_sql = f"SELECT COUNT(*) as total FROM kehu{where_clause}"
            self.cursor.execute(count_sql, params)
            total = self.cursor.fetchone()['total']

            # 查询分页数据 - 保持原样,不做任何修改
            offset = (page - 1) * size
            sql = f"SELECT * FROM kehu{where_clause} ORDER BY submit_time DESC LIMIT %s OFFSET %s"
            self.cursor.execute(sql, params + [size, offset])
            records = self.cursor.fetchall()

            return records, total
        except Error as e:
            print(f"查询失败: {e}")
            return [], 0
        finally:
            self.close()

    def create_customer(self, data):
        """新增客户(完整版)"""
        if not self.connect():
            return False, "数据库连接失败", None

        try:
            sql = """
                INSERT INTO kehu (
                    customer_name, phone, device_model, fault_description,
                    sn_code, customer_email, customer_qq, customer_wechat,
                    emergency_level, source_channel, assign_engineer,
                    repair_cost, repair_result, remark, status, submit_time, update_time
                ) VALUES (
                    %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()
                )
            """
            values = (
                data.get('customer_name'), data.get('phone'), data.get('device_model'), data.get('fault_description'),
                data.get('sn_code'), data.get('customer_email'), data.get('customer_qq'), data.get('customer_wechat'),
                data.get('emergency_level'), data.get('source_channel'), data.get('assign_engineer'),
                data.get('repair_cost'), data.get('repair_result'), data.get('remark'),
                data.get('status', 1)
            )
            self.cursor.execute(sql, values)
            self.conn.commit()
            insert_id = self.cursor.lastrowid
            return True, "添加成功", insert_id
        except Error as e:
            self.conn.rollback()
            print(f"数据库错误详情: {e}")
            return False, f"数据库错误: {e}", None
        except Exception as e:
            self.conn.rollback()
            print(f"错误详情: {e}")
            return False, f"错误: {e}", None
        finally:
            self.close()

    def update_customer(self, record_id, data):
        """更新客户(完整版)"""
        if not self.connect():
            return False, "数据库连接失败"

        try:
            sql = """
                UPDATE kehu SET
                    customer_name=%s, phone=%s, device_model=%s, fault_description=%s,
                    sn_code=%s, customer_email=%s, customer_qq=%s, customer_wechat=%s,
                    status=%s, emergency_level=%s, source_channel=%s, assign_engineer=%s,
                    repair_cost=%s, repair_result=%s, remark=%s, update_time=NOW()
                WHERE id=%s
            """
            values = (
                data.get('customer_name'), data.get('phone'), data.get('device_model'), data.get('fault_description'),
                data.get('sn_code'), data.get('customer_email'), data.get('customer_qq'), data.get('customer_wechat'),
                data.get('status', 1), data.get('emergency_level'), data.get('source_channel'),
                data.get('assign_engineer'),
                data.get('repair_cost'), data.get('repair_result'), data.get('remark'), record_id
            )
            self.cursor.execute(sql, values)
            self.conn.commit()
            return True, "更新成功"
        except Error as e:
            self.conn.rollback()
            print(f"更新失败: {e}")
            return False, f"数据库错误: {e}"
        finally:
            self.close()

    def delete_customer(self, record_id):
        """删除客户"""
        if not self.connect():
            return False, "数据库连接失败"
        try:
            sql = "DELETE FROM kehu WHERE id = %s"
            self.cursor.execute(sql, (record_id,))
            self.conn.commit()
            return True, "删除成功"
        except Error as e:
            self.conn.rollback()
            print(f"删除失败: {e}")
            return False, f"删除失败: {e}"
        finally:
            self.close()


def validate_form_data(data):
    errors = {}

    name = data.get('customer_name', '').strip()
    if not name:
        errors['customer_name'] = '客户姓名不能为空'
    elif len(name) < 2 or len(name) > 30:
        errors['customer_name'] = '姓名长度应为2-30个字符'

    phone = data.get('phone', '').strip()
    phone_pattern = re.compile(r'^1[0-9]{10}$')
    if not phone:
        errors['phone'] = '手机号码不能为空'
    elif not phone_pattern.match(phone):
        errors['phone'] = '手机号格式不正确(11位数字,以1开头)'

    model = data.get('device_model', '').strip()
    if not model:
        errors['device_model'] = '电脑型号不能为空'

    fault = data.get('fault_description', '').strip()
    if not fault:
        errors['fault_description'] = '故障描述不能为空'
    elif len(fault) < 5:
        errors['fault_description'] = '故障描述至少需要5个字符'

    return errors


# ======================================================
# Flask 路由定义
# ======================================================

@app.route('/')
def index():
    """显示表单页面"""
    return render_template('kehu.html')


@app.route('/edit')
def edit_page():
    """客户管理页面"""
    return render_template('kehu-edit.html')


@app.route('/submit', methods=['POST'])
def submit():
    """处理表单提交"""
    print("收到提交请求")
    print("表单数据:", request.form)

    form_data = {
        'customer_name': request.form.get('customer_name', '').strip(),
        'phone': request.form.get('phone', '').strip(),
        'device_model': request.form.get('device_model', '').strip(),
        'fault_description': request.form.get('fault_description', '').strip(),
        'sn_code': request.form.get('sn_code') or None,
        'customer_email': request.form.get('customer_email') or None,
        'customer_qq': request.form.get('customer_qq') or None,
        'customer_wechat': request.form.get('customer_wechat') or None,
    }

    errors = validate_form_data(form_data)
    if errors:
        return jsonify({'success': False, 'errors': errors, 'message': '验证失败'}), 400

    db = KehuDB()
    success, message, insert_id = db.insert_customer(form_data)

    if success:
        return jsonify({'success': True, 'message': '维修申请提交成功!', 'id': insert_id})
    else:
        return jsonify({'success': False, 'message': message}), 500


@app.route('/records', methods=['GET'])
def get_records():
    """获取分页记录(支持搜索)"""
    page = int(request.args.get('page', 1))
    size = int(request.args.get('size', 10))
    name = request.args.get('name', '')
    phone = request.args.get('phone', '')
    status = request.args.get('status', '')

    db = KehuDB()
    records, total = db.get_records_paginated(page, size, name, phone, status)

    # 只在这里格式化时间,不影响查询
    for record in records:
        if record.get('submit_time'):
            if hasattr(record['submit_time'], 'strftime'):
                record['submit_time'] = record['submit_time'].strftime('%Y-%m-%d %H:%M:%S')
        if record.get('update_time'):
            if hasattr(record['update_time'], 'strftime'):
                record['update_time'] = record['update_time'].strftime('%Y-%m-%d %H:%M:%S')

    return jsonify({
        'success': True,
        'data': records,
        'total': total,
        'page': page,
        'size': size,
        'totalPages': (total + size - 1) // size
    })


@app.route('/record/<int:record_id>', methods=['GET'])
def get_record(record_id):
    db = KehuDB()
    record = db.get_record_by_id(record_id)
    if record:
        # 格式化时间
        if record.get('submit_time'):
            if hasattr(record['submit_time'], 'strftime'):
                record['submit_time'] = record['submit_time'].strftime('%Y-%m-%d %H:%M:%S')
        if record.get('update_time'):
            if hasattr(record['update_time'], 'strftime'):
                record['update_time'] = record['update_time'].strftime('%Y-%m-%d %H:%M:%S')
        return jsonify({'success': True, 'data': record})
    return jsonify({'success': False, 'message': '记录不存在'}), 404


@app.route('/record', methods=['POST'])
def create_record():
    """新增客户"""
    data = request.get_json()

    # 验证必填字段
    if not data.get('customer_name') or not data.get('phone') or not data.get('device_model') or not data.get(
            'fault_description'):
        return jsonify({'success': False, 'message': '请填写所有必填项'}), 400

    if not re.match(r'^1[0-9]{10}$', data.get('phone', '')):
        return jsonify({'success': False, 'message': '手机号格式不正确'}), 400

    db = KehuDB()
    success, message, insert_id = db.create_customer(data)

    if success:
        return jsonify({'success': True, 'message': message, 'id': insert_id})
    return jsonify({'success': False, 'message': message}), 500


@app.route('/record/<int:record_id>', methods=['PUT'])
def update_record(record_id):
    """更新客户(完整版)"""
    data = request.get_json()

    # 如果只是更新状态(兼容旧接口)
    if 'status' in data and len(data) == 1:
        status = data.get('status')
        if not status or status not in [1, 2, 3, 4, 5]:
            return jsonify({'success': False, 'message': '无效的状态值'}), 400
        db = KehuDB()
        success = db.update_status(record_id, status)
        if success:
            return jsonify({'success': True, 'message': '状态更新成功'})
        return jsonify({'success': False, 'message': '更新失败'}), 500

    # 完整更新
    db = KehuDB()
    success, message = db.update_customer(record_id, data)

    if success:
        return jsonify({'success': True, 'message': message})
    return jsonify({'success': False, 'message': message}), 500


@app.route('/record/<int:record_id>', methods=['DELETE'])
def delete_record(record_id):
    """删除记录"""
    db = KehuDB()
    success, message = db.delete_customer(record_id)

    if success:
        return jsonify({'success': True, 'message': message})
    return jsonify({'success': False, 'message': message}), 500


@app.route('/search', methods=['GET'])
def search_by_phone():
    phone = request.args.get('phone', '')
    if not phone:
        return jsonify({'success': False, 'message': '请提供手机号'}), 400

    db = KehuDB()
    records = db.get_records_by_phone(phone)
    return jsonify({'success': True, 'data': records})


@app.route('/test-db')
def test_db():
    """测试数据库连接"""
    db = KehuDB()
    if db.connect():
        db.close()
        return jsonify({'success': True, 'message': '数据库连接成功'})
    return jsonify({'success': False, 'message': '数据库连接失败'}), 500


if __name__ == '__main__':
    print("=" * 50)
    print("启动服务器...")
    print("访问地址: http://localhost:5000")
    print("客户登记: http://localhost:5000")
    print("客户管理: http://localhost:5000/edit")
    print("测试数据库: http://localhost:5000/test-db")
    print("=" * 50)
    app.run(debug=True, host='0.0.0.0', port=5000)

ok了

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐