JavaScript实现教培行业意向登记系统

本系统的数据全部自动存储在浏览器自带的 localStorage (本地存储) 里

对应代码就是这两处:

  • 保存: localStorage.setItem('customerData', 数据)

  • 读取: JSON.parse(localStorage.getItem('customerData'))

javascript 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>意向客户跟进系统</title>
    <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; font-family: "Microsoft YaHei", sans-serif; }
        body { background-color: #f5f7fa; color: #333; padding: 20px; transition: background-color 0.3s, color 0.3s; }
        .container { max-width: 1320px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); transition: background-color 0.3s, box-shadow 0.3s; }
        header { text-align: center; margin-bottom: 30px; }
        h1 { color: #2c3e50; transition: color 0.3s; }

        .operation-bar { display: flex; justify-content: space-between; margin-bottom: 20px; flex-wrap: wrap; gap: 10px; align-items: center; }
        .filter-group { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
        .search-box input { padding: 7px 12px; border: 1px solid #ccc; border-radius: 4px; width: 220px; height: 34px; outline: none; transition: border-color 0.3s; }
        .search-box input:focus { border-color: #4e73df; }
        .btn { padding: 7px 12px; border: none; border-radius: 4px; cursor: pointer; color: white; transition: background-color 0.3s; font-size: 14px; height: 34px; }
        .btn-primary { background-color: #4e73df; }
        .btn-success { background-color: #1cc88a; }
        .btn-danger { background-color: #e74a3b; }
        .btn-warning { background-color: #f6c23e; color: #fff; }
        .btn:disabled { background-color: #ccc; cursor: not-allowed; }
        .btn:hover:not(:disabled) { opacity: 0.85; }

        /* 统一下拉框样式 */
        .page-size-select {
            padding: 7px 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 14px;
            width: 110px;
            height: 34px;
            background: white;
            cursor: pointer;
            outline: none;
            transition: border-color 0.3s;
        }
        .page-size-select:focus { border-color: #4e73df; }

        /* 自定义页码跳转下拉 */
        .page-jump-wrap {
            position: relative;
            display: inline-block;
        }
        .page-jump-btn {
            padding: 5px 12px;
            border: 1px solid #ccc;
            border-radius: 4px;
            background: white;
            cursor: pointer;
            font-size: 14px;
            height: 34px;
            min-width: 90px;
            text-align: center;
            transition: border-color 0.3s;
            color: #333;
        }
        .page-jump-btn:hover { border-color: #4e73df; }
        .page-jump-dropdown {
            position: absolute;
            bottom: calc(100% + 4px);
            left: 0;
            background: white;
            border: 1px solid #ccc;
            border-radius: 4px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            max-height: 300px; /* 固定显示10个选项高度 */
            overflow-y: auto;
            z-index: 200;
            display: none;
            min-width: 100%;
        }
        .page-jump-dropdown.show { display: block; }
        .page-jump-option {
            padding: 7px 12px;
            cursor: pointer;
            font-size: 14px;
            border-bottom: 1px solid #f0f0f0;
            text-align: center;
            transition: background-color 0.2s;
        }
        .page-jump-option:hover { background-color: #f5f7fa; }
        .page-jump-option.active { background-color: #4e73df; color: white; }
        .page-jump-option:last-child { border-bottom: none; }

        /* 滚动条美化 */
        .page-jump-dropdown::-webkit-scrollbar { width: 6px; }
        .page-jump-dropdown::-webkit-scrollbar-thumb {
            background: #c1c1c1;
            border-radius: 3px;
        }
        .page-jump-dropdown::-webkit-scrollbar-track { background: #f5f5f5; }

        table { width: 100%; border-collapse: collapse; margin-top: 20px; table-layout: fixed; }
        th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #e0e0e0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: background-color 0.3s, border-color 0.3s; }
        th { background-color: #f8f9fc; color: #6e707e; font-weight: bold; transition: background-color 0.3s, color 0.3s; }
        tr:hover { background-color: #f5f7fa; transition: background-color 0.2s; }
        .today-tag { display: inline-block; padding: 2px 6px; background-color: #1cc88a; color: white; border-radius: 3px; font-size: 12px; margin-left: 6px; }
        .empty-tip { text-align: center; color: #999; padding: 40px 0; white-space: normal; }
        input[type="checkbox"] { cursor: pointer; width: 16px; height: 16px; }

        /* 意向级别标签样式 */
        .intention-tag {
            display: inline-block;
            padding: 2px 8px;
            border-radius: 3px;
            font-size: 12px;
            font-weight: bold;
            color: white;
        }
        .level-a { background-color: #e74a3b; }
        .level-b { background-color: #f6c23e; }
        .level-c { background-color: #1cc88a; }

        /* 固定列宽 */
        th:nth-child(1), td:nth-child(1) { width: 50px; text-align: center; }
        th:nth-child(2), td:nth-child(2) { width: 70px; text-align: center; }
        th:nth-child(3), td:nth-child(3) { width: 120px; }
        th:nth-child(4), td:nth-child(4) { width: 100px; }
        th:nth-child(5), td:nth-child(5) { width: 100px; }
        th:nth-child(6), td:nth-child(6) { width: 80px; }
        th:nth-child(7), td:nth-child(7) { width: 130px; }
        th:nth-child(8), td:nth-child(8) { width: 100px; text-align: center; }
        th:nth-child(9), td:nth-child(9) { width: 80px; text-align: center; }
        th:nth-child(10), td:nth-child(10) { width: 200px; white-space: normal; }

        /* 分页控件样式 */
        .pagination {
            display: flex;
            justify-content: center;
            align-items: center;
            margin-top: 20px;
            gap: 8px;
            flex-wrap: wrap;
        }
        .pagination button {
            padding: 6px 14px;
            border: 1px solid #4e73df;
            background-color: white;
            color: #4e73df;
            border-radius: 4px;
            cursor: pointer;
            transition: 0.3s;
            height: 32px;
        }
        .pagination button:hover:not(:disabled) {
            background-color: #4e73df;
            color: white;
        }
        .pagination button:disabled {
            border-color: #ccc;
            color: #ccc;
            cursor: not-allowed;
        }
        .pagination .page-info {
            color: #666;
            font-size: 14px;
            margin: 0 10px;
        }

        .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; z-index: 1000; }
        .modal-content { background-color: white; padding: 24px; border-radius: 8px; width: 100%; max-width: 500px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); transition: background-color 0.3s; }
        .detail-content { max-width: 550px; }
        .form-group { margin-bottom: 15px; }
        .form-group label { display: block; margin-bottom: 5px; font-weight: bold; color: #5a5c69; transition: color 0.3s; }
        input, textarea, select { width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; transition: background-color 0.3s, border-color 0.3s, color 0.3s; }
        textarea { resize: vertical; min-height: 100px; }
        .action-buttons { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
        .modal-content h2 { color: #2c3e50; margin-bottom: 20px; transition: color 0.3s; }

        /* 单选组横向排列样式 */
        .radio-group {
            display: flex;
            gap: 24px;
            align-items: center;
            flex-wrap: wrap;
        }
        .radio-group label {
            display: inline;
            font-weight: normal;
            margin-bottom: 0;
            margin-left: 4px;
            cursor: pointer;
            color: #333;
        }
        .radio-group input[type="radio"] {
            width: auto;
            margin: 0;
            vertical-align: middle;
            cursor: pointer;
        }

        /* 详情弹窗样式 */
        .detail-item {
            margin-bottom: 14px;
            line-height: 1.6;
        }
        .detail-item .label {
            display: inline-block;
            font-weight: bold;
            color: #5a5c69;
            min-width: 90px;
            transition: color 0.3s;
        }
        .detail-notes .notes-content {
            margin: 6px 0 0 0;
            padding: 10px;
            background-color: #f8f9fc;
            border-radius: 4px;
            max-height: 220px;
            overflow-y: auto;
            white-space: pre-wrap;
            word-break: break-all;
            line-height: 1.6;
            transition: background-color 0.3s;
        }

        /* ========== 主题样式 ========== */
        /* 浅蓝主题 */
        body.theme-blue { background-color: #e6f2ff; }
        body.theme-blue th { background-color: #eaf4ff; color: #2c5aa0; }
        body.theme-blue .container { box-shadow: 0 2px 12px 0 rgba(0, 80, 180, 0.08); }
        body.theme-blue tr:hover { background-color: #f0f7ff; }

        /* 浅紫主题 */
        body.theme-purple { background-color: #f3e8ff; }
        body.theme-purple th { background-color: #f5efff; color: #6b46c1; }
        body.theme-purple tr:hover { background-color: #faf5ff; }

        /* 浅绿主题 */
        body.theme-green { background-color: #e8f8f0; }
        body.theme-green th { background-color: #e6f7ee; color: #2f855a; }
        body.theme-green tr:hover { background-color: #f0faf5; }

        /* 深黑主题 */
        body.theme-dark { background-color: #121217; color: #e4e4eb; }
        body.theme-dark .container { background: #1e1e2a; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.4); }
        body.theme-dark h1,
        body.theme-dark .modal-content h2 { color: #e4e4eb; }
        body.theme-dark th { background-color: #2a2a3a; color: #c9c9d6; border-bottom-color: #3a3a4a; }
        body.theme-dark td { border-bottom-color: #2d2d3d; }
        body.theme-dark tr:hover { background-color: #272736; }
        body.theme-dark input,
        body.theme-dark select,
        body.theme-dark textarea,
        body.theme-dark .page-size-select,
        body.theme-dark .page-jump-btn {
            background-color: #2a2a3a;
            border-color: #3a3a4a;
            color: #e4e4eb;
        }
        body.theme-dark .form-group label,
        body.theme-dark .detail-item .label { color: #c9c9d6; }
        body.theme-dark .radio-group label { color: #e4e4eb; }
        body.theme-dark .modal-content { background-color: #1e1e2a; }
        body.theme-dark .detail-notes .notes-content { background-color: #2a2a3a; }
        body.theme-dark .empty-tip { color: #888; }
        body.theme-dark .pagination button { background-color: #2a2a3a; border-color: #3a3a4a; color: #e4e4eb; }
        body.theme-dark .pagination button:hover:not(:disabled) { background-color: #4e73df; border-color: #4e73df; color: white; }
        body.theme-dark .page-info { color: #aaa; }
        body.theme-dark .page-jump-dropdown {
            background-color: #1e1e2a;
            border-color: #3a3a4a;
        }
        body.theme-dark .page-jump-option { border-bottom-color: #2d2d3d; }
        body.theme-dark .page-jump-option:hover { background-color: #272736; }
        body.theme-dark .page-jump-dropdown::-webkit-scrollbar-track { background: #2a2a3a; }
        body.theme-dark .page-jump-dropdown::-webkit-scrollbar-thumb { background: #4a4a5a; }
    </style>
</head>
<body class="theme-classic">
    <div class="container">
        <header>
            <h1>意向客户跟进系统</h1>
        </header>

        <div class="operation-bar">
            <div class="filter-group">
                <button class="btn btn-primary" onclick="openAddModal()">添加客户</button>
                <button class="btn btn-danger" id="batchDeleteBtn" onclick="batchDelete()" disabled>批量删除</button>
                <button class="btn btn-warning" onclick="toggleTodayFilter()" id="todayFilterBtn">今日回访</button>
                <button class="btn btn-success" onclick="exportData()">导出Excel</button>
                <input type="file" id="fileInput" accept=".xlsx,.xls" style="display: none;" onchange="importData(event)">
                <button class="btn btn-danger" onclick="document.getElementById('fileInput').click()">导入Excel</button>
            </div>
            <div class="filter-group">
                <div class="search-box">
                    <input type="text" id="searchInput" placeholder="搜索姓名/手机号/备注..." oninput="handleSearch()">
                </div>
                <select id="regionFilter" class="page-size-select" onchange="handleRegionFilter()">
                    <option value="all">全部地区</option>
                </select>
                <select id="intentionFilter" class="page-size-select" onchange="handleIntentionFilter()">
                    <option value="all">全部级别</option>
                    <option value="A">A级意向</option>
                    <option value="B">B级意向</option>
                    <option value="C">C级意向</option>
                </select>
                <select id="themeSelect" class="page-size-select" onchange="changeTheme(this.value)">
                    <option value="classic">经典主题</option>
                    <option value="blue">浅蓝主题</option>
                    <option value="purple">浅紫主题</option>
                    <option value="green">浅绿主题</option>
                    <option value="dark">深黑主题</option>
                </select>
            </div>
        </div>

        <table id="customerTable">
            <thead>
                <tr>
                    <th><input type="checkbox" id="selectAll" onchange="handleSelectAll()"></th>
                    <th>序号</th>
                    <th>回访日期</th>
                    <th>地区</th>
                    <th>学生姓名</th>
                    <th>联系人</th>
                    <th>手机号</th>
                    <th>意向级别</th>
                    <th>跟进次数</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                <!-- 数据将由 JS 动态填充 -->
            </tbody>
        </table>

        <!-- 分页控件 -->
        <div class="pagination" id="pagination"></div>

        <!-- 添加/编辑客户模态框 -->
        <div id="customerModal" class="modal">
            <div class="modal-content">
                <h2 id="modalTitle">添加客户</h2>
                <form id="customerForm" onsubmit="return saveCustomer()">
                    <input type="hidden" id="editId">
                    <div class="form-group">
                        <label for="date">录入日期:</label>
                        <input type="date" id="date" required>
                    </div>
                    <div class="form-group">
                        <label for="followUpDate">回访日期:</label>
                        <input type="date" id="followUpDate">
                    </div>
                    <div class="form-group">
                        <label for="region">地区:</label>
                        <input type="text" id="region" placeholder="请输入地区" required>
                    </div>
                    <div class="form-group">
                        <label for="name">学生姓名:</label>
                        <input type="text" id="name" placeholder="请输入学生姓名" required>
                    </div>
                    <div class="form-group">
                        <label>联系人:</label>
                        <div class="radio-group">
                            <div>
                                <input type="radio" id="genderMale" name="gender" value="男" required>
                                <label for="genderMale">爸爸</label>
                            </div>
                            <div>
                                <input type="radio" id="genderFemale" name="gender" value="女">
                                <label for="genderFemale">妈妈</label>
                            </div>
                        </div>
                    </div>
                    <div class="form-group">
                        <label for="phone">手机号:</label>
                        <input type="tel" id="phone" placeholder="请输入11位手机号" maxlength="11" pattern="[0-9]{11}">
                    </div>
                    <div class="form-group">
                        <label>意向级别:</label>
                        <div class="radio-group">
                            <div>
                                <input type="radio" id="intentionA" name="intentionLevel" value="A" required>
                                <label for="intentionA">A</label>
                            </div>
                            <div>
                                <input type="radio" id="intentionB" name="intentionLevel" value="B">
                                <label for="intentionB">B</label>
                            </div>
                            <div>
                                <input type="radio" id="intentionC" name="intentionLevel" value="C">
                                <label for="intentionC">C</label>
                            </div>
                        </div>
                    </div>
                    <div class="form-group">
                        <label for="notes">备注:</label>
                        <textarea id="notes" placeholder="请输入跟进备注"></textarea>
                    </div>
                    <div class="form-group">
                        <label for="followUpCount">跟进次数:</label>
                        <input type="number" id="followUpCount" min="0" value="0" required>
                    </div>
                    <div class="action-buttons">
                        <button type="button" class="btn btn-danger" onclick="closeModal()">取消</button>
                        <button type="submit" class="btn btn-primary">保存</button>
                    </div>
                </form>
            </div>
        </div>

        <!-- 客户详情模态框 -->
        <div id="detailModal" class="modal">
            <div class="modal-content detail-content">
                <h2>客户详情</h2>
                <div class="detail-item">
                    <span class="label">录入序号:</span>
                    <span id="detailSerial"></span>
                </div>
                <div class="detail-item">
                    <span class="label">录入日期:</span>
                    <span id="detailDate"></span>
                </div>
                <div class="detail-item">
                    <span class="label">回访日期:</span>
                    <span id="detailFollowUpDate"></span>
                </div>
                <div class="detail-item">
                    <span class="label">地区:</span>
                    <span id="detailRegion"></span>
                </div>
                <div class="detail-item">
                    <span class="label">学生姓名:</span>
                    <span id="detailName"></span>
                </div>
                <div class="detail-item">
                    <span class="label">联系人:</span>
                    <span id="detailContact"></span>
                </div>
                <div class="detail-item">
                    <span class="label">手机号:</span>
                    <span id="detailPhone"></span>
                </div>
                <div class="detail-item">
                    <span class="label">意向级别:</span>
                    <span id="detailIntention"></span>
                </div>
                <div class="detail-item detail-notes">
                    <span class="label">备注:</span>
                    <p class="notes-content" id="detailNotes"></p>
                </div>
                <div class="detail-item">
                    <span class="label">跟进次数:</span>
                    <span id="detailFollowUpCount"></span>
                </div>
                <div class="action-buttons">
                    <button type="button" class="btn btn-primary" onclick="closeDetailModal()">关闭</button>
                </div>
            </div>
        </div>
    </div>

    <script>
        // 1. 数据初始化与兼容处理
        let customers = JSON.parse(localStorage.getItem('customerData')) || [];

        // 兼容旧数据:自动补全意向级别和录入序号
        (function initData() {
            let maxSerial = 0;
            customers = customers.map((item, index) => {
                if (!item.intentionLevel) item.intentionLevel = 'A';
                if (!item.serialNumber) item.serialNumber = index + 1;
                if (item.serialNumber > maxSerial) maxSerial = item.serialNumber;
                return item;
            });
            window.nextSerialNumber = maxSerial + 1;
        })();

        // 主题初始化
        const savedTheme = localStorage.getItem('customerTheme') || 'classic';
        document.body.className = 'theme-' + savedTheme;

        let editingId = null;
        let currentPage = 1;
        let pageSize = 10;
        let searchKeyword = '';
        let isTodayFilter = false;
        let selectedIds = [];
        let intentionFilter = 'all';
        let regionFilter = 'all';

        window.onload = function() {
            document.getElementById('themeSelect').value = savedTheme;
            updateRegionFilterOptions();
            renderTable();
        };

        // 切换主题
        function changeTheme(theme) {
            document.body.className = 'theme-' + theme;
            localStorage.setItem('customerTheme', theme);
        }

        // 获取今日日期字符串
        function getTodayString() {
            const today = new Date();
            const year = today.getFullYear();
            const month = String(today.getMonth() + 1).padStart(2, '0');
            const day = String(today.getDate()).padStart(2, '0');
            return `${year}-${month}-${day}`;
        }

        // 切换今日回访筛选
        function toggleTodayFilter() {
            isTodayFilter = !isTodayFilter;
            document.getElementById('todayFilterBtn').textContent = isTodayFilter ? '显示全部' : '今日回访';
            currentPage = 1;
            selectedIds = [];
            closePageJumpDropdown();
            renderTable();
        }

        // 意向级别筛选处理
        function handleIntentionFilter() {
            intentionFilter = document.getElementById('intentionFilter').value;
            currentPage = 1;
            selectedIds = [];
            closePageJumpDropdown();
            renderTable();
        }

        // 地区筛选处理
        function handleRegionFilter() {
            regionFilter = document.getElementById('regionFilter').value;
            currentPage = 1;
            selectedIds = [];
            closePageJumpDropdown();
            renderTable();
        }

        // 更新地区下拉选项
        function updateRegionFilterOptions() {
            const regionSelect = document.getElementById('regionFilter');
            const currentSelected = regionSelect.value;
            
            const uniqueRegions = [...new Set(
                customers
                    .map(c => c.region?.trim())
                    .filter(region => region)
            )].sort((a, b) => a.localeCompare(b, 'zh-CN'));

            let options = '<option value="all">全部地区</option>';
            uniqueRegions.forEach(region => {
                options += `<option value="${region}">${region}</option>`;
            });

            regionSelect.innerHTML = options;
            if (uniqueRegions.includes(currentSelected)) {
                regionSelect.value = currentSelected;
            } else {
                regionSelect.value = 'all';
                regionFilter = 'all';
            }
        }

        // 切换页码跳转下拉
        function togglePageJumpDropdown(event) {
            event.stopPropagation();
            const dropdown = document.getElementById('pageJumpDropdown');
            dropdown.classList.toggle('show');
            // 展开时自动滚动到当前选中项
            if (dropdown.classList.contains('show')) {
                const activeOption = dropdown.querySelector('.page-jump-option.active');
                if (activeOption) {
                    activeOption.scrollIntoView({ block: 'center' });
                }
            }
        }

        // 关闭页码跳转下拉
        function closePageJumpDropdown() {
            const dropdown = document.getElementById('pageJumpDropdown');
            if (dropdown) dropdown.classList.remove('show');
        }

        // 全选/取消全选当前页
        function handleSelectAll() {
            const isChecked = document.getElementById('selectAll').checked;
            const filteredData = getFilteredData();
            const sortedData = getSortedData(filteredData);
            const startIndex = (currentPage - 1) * pageSize;
            const endIndex = Math.min(startIndex + pageSize, sortedData.length);
            const currentPageIds = sortedData.slice(startIndex, endIndex).map(c => c.id);

            if (isChecked) {
                currentPageIds.forEach(id => {
                    if (!selectedIds.includes(id)) selectedIds.push(id);
                });
            } else {
                selectedIds = selectedIds.filter(id => !currentPageIds.includes(id));
            }

            updateBatchBtnState();
            renderTable();
        }

        // 单行选中/取消
        function handleRowSelect(id) {
            if (selectedIds.includes(id)) {
                selectedIds = selectedIds.filter(item => item !== id);
            } else {
                selectedIds.push(id);
            }
            updateBatchBtnState();
            updateSelectAllState();
        }

        // 更新全选框状态
        function updateSelectAllState() {
            const filteredData = getFilteredData();
            const sortedData = getSortedData(filteredData);
            const startIndex = (currentPage - 1) * pageSize;
            const endIndex = Math.min(startIndex + pageSize, sortedData.length);
            const currentPageIds = sortedData.slice(startIndex, endIndex).map(c => c.id);

            const allChecked = currentPageIds.length > 0 && currentPageIds.every(id => selectedIds.includes(id));
            document.getElementById('selectAll').checked = allChecked;
        }

        // 更新批量删除按钮状态
        function updateBatchBtnState() {
            document.getElementById('batchDeleteBtn').disabled = selectedIds.length === 0;
        }

        // 批量删除
        function batchDelete() {
            if (selectedIds.length === 0) {
                alert('请先选择要删除的客户');
                return;
            }
            if (confirm(`确定要删除选中的 ${selectedIds.length} 条客户信息吗?此操作不可恢复。`)) {
                customers = customers.filter(c => !selectedIds.includes(c.id));
                selectedIds = [];
                const totalPages = getTotalPages();
                if (currentPage > totalPages) currentPage = totalPages;
                updateRegionFilterOptions();
                saveData();
                renderTable();
            }
        }

        // 全字段模糊搜索 + 多条件筛选
        function getFilteredData() {
            let result = [...customers];

            if (isTodayFilter) {
                const today = getTodayString();
                result = result.filter(c => c.followUpDate === today);
            }

            if (intentionFilter !== 'all') {
                result = result.filter(c => c.intentionLevel === intentionFilter);
            }

            if (regionFilter !== 'all') {
                result = result.filter(c => c.region === regionFilter);
            }

            if (searchKeyword.trim()) {
                const keyword = searchKeyword.trim().toLowerCase();
                result = result.filter(c => {
                    const name = String(c.name || '').toLowerCase();
                    const phone = String(c.phone || '').toLowerCase();
                    const region = String(c.region || '').toLowerCase();
                    const notes = String(c.notes || '').toLowerCase();
                    const contact = c.gender === '男' ? '爸爸' : '妈妈';
                    const intention = String(c.intentionLevel || '').toLowerCase();
                    const serial = String(c.serialNumber || '').toLowerCase();

                    return name.includes(keyword)
                        || phone.includes(keyword)
                        || region.includes(keyword)
                        || notes.includes(keyword)
                        || contact.includes(keyword)
                        || intention.includes(keyword)
                        || serial.includes(keyword);
                });
            }

            return result;
        }

        // 固定排序规则:按录入序号降序
        function getSortedData(data) {
            return [...data].sort((a, b) => b.serialNumber - a.serialNumber);
        }

        // 搜索输入处理
        function handleSearch() {
            searchKeyword = document.getElementById('searchInput').value;
            currentPage = 1;
            selectedIds = [];
            closePageJumpDropdown();
            renderTable();
        }

        // 计算总页数
        function getTotalPages() {
            const filtered = getFilteredData();
            return Math.ceil(filtered.length / pageSize) || 1;
        }

        // 渲染客户表格
        function renderTable() {
            const tbody = document.getElementById('customerTable').getElementsByTagName('tbody')[0];
            tbody.innerHTML = '';

            const filteredData = getFilteredData();
            const sortedData = getSortedData(filteredData);
            const today = getTodayString();

            const totalPages = getTotalPages();
            if (currentPage > totalPages) currentPage = totalPages;
            if (currentPage < 1) currentPage = 1;

            const startIndex = (currentPage - 1) * pageSize;
            const endIndex = Math.min(startIndex + pageSize, sortedData.length);
            const pageData = sortedData.slice(startIndex, endIndex);

            if (pageData.length === 0) {
                const row = tbody.insertRow();
                const cell = row.insertCell(0);
                cell.colSpan = 10;
                cell.className = 'empty-tip';
                if (isTodayFilter && searchKeyword.trim()) {
                    cell.textContent = '今日回访中未找到匹配结果';
                } else if (isTodayFilter) {
                    cell.textContent = '今日暂无待回访客户';
                } else if (searchKeyword.trim()) {
                    cell.textContent = '未找到匹配的客户信息';
                } else {
                    cell.textContent = '暂无数据,请添加客户';
                }
                renderPagination();
                updateSelectAllState();
                updateBatchBtnState();
                return;
            }

            pageData.forEach(customer => {
                const row = tbody.insertRow();
                const checkboxCell = row.insertCell(0);
                checkboxCell.innerHTML = `<input type="checkbox" class="row-checkbox" 
                    ${selectedIds.includes(customer.id) ? 'checked' : ''} 
                    onchange="handleRowSelect(${customer.id})">`;

                row.insertCell(1).textContent = customer.serialNumber;

                const followUpCell = row.insertCell(2);
                const dateText = customer.followUpDate || '-';
                if (customer.followUpDate === today) {
                    followUpCell.innerHTML = dateText + '<span class="today-tag">今日</span>';
                } else {
                    followUpCell.textContent = dateText;
                }

                row.insertCell(3).textContent = customer.region;
                row.insertCell(4).textContent = customer.name;
                row.insertCell(5).textContent = customer.gender === '男' ? '爸爸' : '妈妈';
                row.insertCell(6).textContent = customer.phone || '-';
                
                const intentionCell = row.insertCell(7);
                const level = customer.intentionLevel || 'A';
                const levelClass = level === 'A' ? 'level-a' : level === 'B' ? 'level-b' : 'level-c';
                intentionCell.innerHTML = `<span class="intention-tag ${levelClass}">${level}</span>`;

                row.insertCell(8).textContent = customer.followUpCount;
                row.insertCell(9).innerHTML = `
                    <div class="actions" style="display: flex; gap: 6px; flex-wrap: wrap;">
                        <button class="btn btn-success" onclick="openDetailModal(${customer.id})">详情</button>
                        <button class="btn btn-primary" onclick="openEditModal(${customer.id})">编辑</button>
                        <button class="btn btn-danger" onclick="deleteCustomer(${customer.id})">删除</button>
                    </div>
                `;
            });

            renderPagination();
            updateSelectAllState();
            updateBatchBtnState();
        }

        // 打开详情弹窗
        function openDetailModal(id) {
            const customer = customers.find(c => c.id === id);
            if (!customer) return;

            document.getElementById('detailSerial').textContent = customer.serialNumber;
            document.getElementById('detailDate').textContent = customer.date;
            document.getElementById('detailFollowUpDate').textContent = customer.followUpDate || '未设置';
            document.getElementById('detailRegion').textContent = customer.region;
            document.getElementById('detailName').textContent = customer.name;
            document.getElementById('detailContact').textContent = customer.gender === '男' ? '爸爸' : '妈妈';
            document.getElementById('detailPhone').textContent = customer.phone || '-';
            document.getElementById('detailIntention').textContent = customer.intentionLevel || 'A';
            document.getElementById('detailNotes').textContent = customer.notes || '无';
            document.getElementById('detailFollowUpCount').textContent = customer.followUpCount + ' 次';

            document.getElementById('detailModal').style.display = 'flex';
        }

        function closeDetailModal() {
            document.getElementById('detailModal').style.display = 'none';
        }

        // 渲染分页控件
        function renderPagination() {
            const totalPages = getTotalPages();
            const filteredData = getFilteredData();
            const pagination = document.getElementById('pagination');

            let html = '';
            html += '<button onclick="goToPage(' + (currentPage - 1) + ')" ' + (currentPage === 1 ? 'disabled' : '') + '>上一页</button>';
            html += '<span class="page-info">第 ' + currentPage + ' 页 / 共 ' + totalPages + ' 页(共 ' + filteredData.length + ' 条)</span>';
            html += '<button onclick="goToPage(' + (currentPage + 1) + ')" ' + (currentPage === totalPages ? 'disabled' : '') + '>下一页</button>';

            // 自定义页码跳转下拉(固定显示10个选项,超出滚动)
            html += '<span class="page-info">跳转:</span>';
            html += '<div class="page-jump-wrap">';
            html += '<button type="button" class="page-jump-btn" onclick="togglePageJumpDropdown(event)">第' + currentPage + '页</button>';
            html += '<div class="page-jump-dropdown" id="pageJumpDropdown">';
            for (let i = 1; i <= totalPages; i++) {
                html += '<div class="page-jump-option ' + (i === currentPage ? 'active' : '') + '" onclick="goToPage(' + i + ')">第' + i + '页</div>';
            }
            html += '</div>';
            html += '</div>';

            html += '<span class="page-info">每页:</span>';
            html += '<select class="page-size-select" onchange="changePageSize(parseInt(this.value))">';
            [5, 10, 20, 50].forEach(size => {
                html += '<option value="' + size + '"' + (size === pageSize ? ' selected' : '') + '>' + size + '条</option>';
            });
            html += '</select>';

            pagination.innerHTML = html;
        }

        function goToPage(page) {
            const totalPages = getTotalPages();
            if (page < 1 || page > totalPages) return;
            currentPage = page;
            closePageJumpDropdown();
            renderTable();
        }

        function changePageSize(newSize) {
            pageSize = newSize;
            currentPage = 1;
            closePageJumpDropdown();
            renderTable();
        }

        // 打开添加模态框
        function openAddModal() {
            document.getElementById('modalTitle').textContent = '添加客户';
            document.getElementById('customerForm').reset();
            document.getElementById('date').value = getTodayString();
            document.getElementById('intentionA').checked = true;
            editingId = null;
            document.getElementById('customerModal').style.display = 'flex';
        }

        // 打开编辑模态框
        function openEditModal(id) {
            const customer = customers.find(c => c.id === id);
            if (customer) {
                document.getElementById('modalTitle').textContent = '编辑客户';
                document.getElementById('editId').value = customer.id;
                document.getElementById('date').value = customer.date;
                document.getElementById('followUpDate').value = customer.followUpDate || '';
                document.getElementById('region').value = customer.region;
                document.getElementById('name').value = customer.name;
                document.getElementById('gender' + (customer.gender === '男' ? 'Male' : 'Female')).checked = true;
                document.getElementById('phone').value = customer.phone || '';
                const level = customer.intentionLevel || 'A';
                document.getElementById('intention' + level).checked = true;
                document.getElementById('notes').value = customer.notes || '';
                document.getElementById('followUpCount').value = customer.followUpCount;
                editingId = id;
                document.getElementById('customerModal').style.display = 'flex';
            }
        }

        function closeModal() {
            document.getElementById('customerModal').style.display = 'none';
        }

        // 保存客户
        function saveCustomer() {
            const id = editingId || new Date().getTime();
            const date = document.getElementById('date').value;
            const followUpDate = document.getElementById('followUpDate').value || '';
            const region = document.getElementById('region').value.trim();
            const name = document.getElementById('name').value;
            const gender = document.querySelector('input[name="gender"]:checked').value;
            const phone = document.getElementById('phone').value.trim();
            const intentionLevel = document.querySelector('input[name="intentionLevel"]:checked').value;
            const notes = document.getElementById('notes').value;
            const followUpCount = parseInt(document.getElementById('followUpCount').value) || 0;

            let customer;
            if (editingId) {
                const oldCustomer = customers.find(c => c.id === editingId);
                customer = {
                    ...oldCustomer,
                    date, followUpDate, region, name, gender,
                    phone, intentionLevel, notes, followUpCount
                };
                const index = customers.findIndex(c => c.id === editingId);
                if (index !== -1) customers[index] = customer;
            } else {
                const serialNumber = window.nextSerialNumber;
                window.nextSerialNumber++;
                customer = {
                    id, serialNumber, date, followUpDate, region, name,
                    gender, phone, intentionLevel, notes, followUpCount
                };
                customers.push(customer);
            }

            updateRegionFilterOptions();
            saveData();
            renderTable();
            closeModal();
            return false;
        }

        // 删除单个客户
        function deleteCustomer(id) {
            if (confirm('确定要删除这条客户信息吗?')) {
                customers = customers.filter(customer => customer.id !== id);
                selectedIds = selectedIds.filter(item => item !== id);
                const totalPages = getTotalPages();
                if (currentPage > totalPages) currentPage = totalPages;
                updateRegionFilterOptions();
                saveData();
                renderTable();
            }
        }

        // 导出Excel
        function exportData() {
            if (customers.length === 0) {
                alert('暂无数据可导出!');
                return;
            }

            const exportList = [...customers].sort((a, b) => a.serialNumber - b.serialNumber);
            const exportData = exportList.map(c => ({
                '录入序号': c.serialNumber,
                '录入日期': c.date,
                '回访日期': c.followUpDate || '',
                '地区': c.region,
                '学生姓名': c.name,
                '联系人': c.gender === '男' ? '爸爸' : '妈妈',
                '手机号': c.phone || '',
                '意向级别': c.intentionLevel || 'A',
                '备注': c.notes || '',
                '跟进次数': c.followUpCount
            }));

            const ws = XLSX.utils.json_to_sheet(exportData);
            ws['!cols'] = [
                { wch: 10 }, { wch: 14 }, { wch: 14 }, { wch: 12 }, { wch: 12 },
                { wch: 10 }, { wch: 15 }, { wch: 12 }, { wch: 25 }, { wch: 10 }
            ];

            const wb = XLSX.utils.book_new();
            XLSX.utils.book_append_sheet(wb, ws, '客户数据');
            XLSX.writeFile(wb, 'customer_data.xlsx');
            alert('数据已导出为 customer_data.xlsx');
        }

        // 导入Excel
        function importData(event) {
            const file = event.target.files[0];
            if (!file) return;

            if (!confirm('导入后会覆盖掉以前的全部数据,您确认继续吗?')) {
                event.target.value = '';
                return;
            }

            const reader = new FileReader();
            reader.onload = function(e) {
                try {
                    const data = e.target.result;
                    const wb = XLSX.read(data, { type: 'array' });
                    const ws = wb.Sheets[wb.SheetNames[0]];
                    const jsonData = XLSX.utils.sheet_to_json(ws);

                    let maxSerial = 0;
                    customers = jsonData.map((row, index) => {
                        let serialNumber = row['录入序号'] ? Number(row['录入序号']) : index + 1;
                        if (serialNumber > maxSerial) maxSerial = serialNumber;
                        return {
                            id: row.id || new Date().getTime() + index,
                            serialNumber: serialNumber,
                            date: row['录入日期'] || row['日期'] || '',
                            followUpDate: row['回访日期'] || '',
                            region: row['地区'] || '',
                            name: row['学生姓名'] || '',
                            gender: row['联系人'] === '爸爸' ? '男' : (row['联系人'] === '妈妈' ? '女' : '男'),
                            phone: row['手机号'] || '',
                            intentionLevel: row['意向级别'] || 'A',
                            notes: row['备注'] || '',
                            followUpCount: parseInt(row['跟进次数']) || 0
                        };
                    });

                    window.nextSerialNumber = maxSerial + 1;
                    currentPage = 1;
                    selectedIds = [];
                    document.getElementById('searchInput').value = '';
                    searchKeyword = '';
                    isTodayFilter = false;
                    document.getElementById('todayFilterBtn').textContent = '今日回访';
                    intentionFilter = 'all';
                    document.getElementById('intentionFilter').value = 'all';
                    regionFilter = 'all';
                    updateRegionFilterOptions();
                    saveData();
                    renderTable();
                    alert('数据导入成功!共导入 ' + customers.length + ' 条记录。');
                } catch (err) {
                    alert('解析失败:请确保Excel文件内容格式正确。');
                    console.error(err);
                }
            };
            reader.readAsArrayBuffer(file);
            event.target.value = '';
        }

        // 保存到 localStorage
        function saveData() {
            localStorage.setItem('customerData', JSON.stringify(customers));
        }

        // 点击页面其他区域关闭弹窗和下拉
        window.onclick = function(event) {
            if (event.target.id === 'customerModal') closeModal();
            if (event.target.id === 'detailModal') closeDetailModal();
            // 关闭页码跳转下拉
            if (!event.target.closest('.page-jump-wrap')) {
                closePageJumpDropdown();
            }
        }
    </script>
</body>
</html>
相关推荐
zzzll11114 小时前
Claude Code离线安装方案揭秘
开发语言·php
wling03015 小时前
vasp虚频计算-python脚本
开发语言·windows·python·vasp计算
郝学胜-神的一滴5 小时前
Qt 高级编程 040:按钮悬浮弹出滑块弹窗的完整攻略
开发语言·c++·qt·软件工程·用户界面
zzzzzz3105 小时前
从 react-bits 看动效组件化:别把视觉效果写成一次性页面代码
javascript·react.js·开源
用户938515635075 小时前
React Context 与自定义 Hook 从底层到实践:「跨层级通信 + 副作用封装」全解析
前端·javascript·react.js
滴滴答答哒6 小时前
VUE3+element-plus MultiSelect 多选下拉组件
前端·javascript·vue.js
xrandzj6 小时前
Python面向对象编程入门:类、实例、初始化与封装实践
开发语言·python
其美杰布-富贵-李6 小时前
04 watch 与 Vue 响应式数据流
前端·javascript·vue.js