document.addEventListener('DOMContentLoaded', function () { // 登录守卫 const token = localStorage.getItem('crmToken'); if (!token && !window.location.pathname.endsWith('login.html')) { window.location.href = '/static/login.html'; return; } // 封装带 Token 的 fetch async function authenticatedFetch(url, options = {}) { const token = localStorage.getItem('crmToken'); const headers = { ...options.headers, 'Authorization': `Bearer ${token}` }; const response = await fetch(url, { ...options, headers }); if (response.status === 401) { localStorage.removeItem('crmToken'); window.location.href = '/static/login.html'; return; } return response; } // Navigation const navItems = document.querySelectorAll('.nav-item'); const customerSection = document.getElementById('customerSection'); const dashboardSection = document.getElementById('dashboardSection'); const pageTitle = document.getElementById('pageTitle'); // Elements const createCustomerForm = document.getElementById('createCustomerForm'); const importFileForm = document.getElementById('importFileForm'); const customerTableBody = document.getElementById('customerTableBody'); const addCustomerBtn = document.getElementById('addCustomerBtn'); const importBtn = document.getElementById('importBtn'); const createModal = document.getElementById('createModal'); const importModal = document.getElementById('importModal'); const editModal = document.getElementById('editModal'); const editCustomerForm = document.getElementById('editCustomerForm'); const menuToggle = document.getElementById('menuToggle'); const sidebar = document.querySelector('.sidebar'); const customerFilter = document.getElementById('customerFilter'); const customerSearchInput = document.getElementById('customerSearchInput'); const contentArea = document.querySelector('.content-area'); const refreshCustomersBtn = document.getElementById('refreshCustomersBtn'); const exportCustomersBtn = document.getElementById('exportCustomersBtn'); let allCustomers = []; let filteredCustomers = []; let dashboardCustomers = []; // Chart instances let statusChartInstance = null; let typeChartInstance = null; // Current section tracking let currentSection = 'customer'; // Pagination state let currentPage = 1; let pageSize = 10; let totalPages = 1; let totalItems = 0; let selectedCustomerFilter = ''; let customerSearchQuery = ''; let cellTooltipEl = null; let tooltipAnchorCell = null; let tooltipHideTimer = null; function ensureCellTooltip() { if (cellTooltipEl) return cellTooltipEl; cellTooltipEl = document.createElement('div'); cellTooltipEl.className = 'cell-tooltip'; cellTooltipEl.style.display = 'none'; document.body.appendChild(cellTooltipEl); cellTooltipEl.addEventListener('mouseenter', () => { if (tooltipHideTimer) { clearTimeout(tooltipHideTimer); tooltipHideTimer = null; } }); cellTooltipEl.addEventListener('mouseleave', (e) => { const nextTarget = e.relatedTarget; if (tooltipAnchorCell && nextTarget && tooltipAnchorCell.contains(nextTarget)) return; hideCellTooltip(0); }); return cellTooltipEl; } function hideCellTooltip(delayMs = 120) { if (tooltipHideTimer) clearTimeout(tooltipHideTimer); tooltipHideTimer = setTimeout(() => { if (!cellTooltipEl) return; cellTooltipEl.style.display = 'none'; cellTooltipEl.textContent = ''; cellTooltipEl.removeAttribute('data-placement'); tooltipAnchorCell = null; }, delayMs); } function positionCellTooltipForCell(cell) { if (!cellTooltipEl) return; const rect = cell.getBoundingClientRect(); const viewportPadding = 12; const offset = 10; cellTooltipEl.style.left = '0px'; cellTooltipEl.style.top = '0px'; cellTooltipEl.style.visibility = 'hidden'; cellTooltipEl.style.display = 'block'; const tipRect = cellTooltipEl.getBoundingClientRect(); const tipWidth = tipRect.width; const tipHeight = tipRect.height; const canShowAbove = rect.top >= tipHeight + offset + viewportPadding; const placement = canShowAbove ? 'top' : 'bottom'; cellTooltipEl.setAttribute('data-placement', placement); const leftUnclamped = rect.left + rect.width / 2 - tipWidth / 2; const left = Math.max(viewportPadding, Math.min(leftUnclamped, window.innerWidth - viewportPadding - tipWidth)); const top = placement === 'top' ? rect.top - tipHeight - offset : Math.min(rect.bottom + offset, window.innerHeight - viewportPadding - tipHeight); cellTooltipEl.style.left = `${Math.round(left)}px`; cellTooltipEl.style.top = `${Math.round(top)}px`; cellTooltipEl.style.visibility = 'visible'; } function showCellTooltipForCell(cell) { const rawText = (cell.getAttribute('data-tooltip') || '').trim(); if (!rawText) return; const tip = ensureCellTooltip(); tip.textContent = rawText; tooltipAnchorCell = cell; if (tooltipHideTimer) { clearTimeout(tooltipHideTimer); tooltipHideTimer = null; } positionCellTooltipForCell(cell); } function bindCellTooltipEvents() { customerTableBody.addEventListener('mouseover', (e) => { const cell = e.target && e.target.closest ? e.target.closest('td.has-overflow') : null; if (!cell || !customerTableBody.contains(cell)) return; if (tooltipAnchorCell === cell && cellTooltipEl && cellTooltipEl.style.display === 'block') return; showCellTooltipForCell(cell); }); customerTableBody.addEventListener('mouseout', (e) => { const fromCell = e.target && e.target.closest ? e.target.closest('td.has-overflow') : null; if (!fromCell) return; const nextTarget = e.relatedTarget; if (cellTooltipEl && nextTarget && cellTooltipEl.contains(nextTarget)) return; if (tooltipAnchorCell === fromCell) hideCellTooltip(120); }); const reposition = () => { if (!cellTooltipEl || cellTooltipEl.style.display !== 'block' || !tooltipAnchorCell) return; positionCellTooltipForCell(tooltipAnchorCell); }; window.addEventListener('resize', reposition); window.addEventListener('scroll', reposition, true); if (contentArea) contentArea.addEventListener('scroll', reposition, { passive: true }); } bindCellTooltipEvents(); function normalizeDateValue(dateValue) { const raw = String(dateValue || '').trim(); if (!raw) return ''; const cleaned = raw.replace(/\./g, '-').replace(/\//g, '-'); const parts = cleaned.split('-').filter(Boolean); if (parts.length < 3) return ''; const year = parts[0].padStart(4, '0'); const month = parts[1].padStart(2, '0'); const day = parts[2].padStart(2, '0'); return `${year}-${month}-${day}`; } // Navigation event listeners navItems.forEach(item => { item.addEventListener('click', function (e) { e.preventDefault(); const section = this.getAttribute('data-section'); switchSection(section); }); }); // Menu toggle for mobile menuToggle.addEventListener('click', function () { sidebar.classList.toggle('open'); }); // Close sidebar when clicking outside on mobile document.addEventListener('click', function (e) { if (window.innerWidth <= 768) { if (!sidebar.contains(e.target) && !menuToggle.contains(e.target)) { sidebar.classList.remove('open'); } } }); // Add Customer button addCustomerBtn.addEventListener('click', function () { createModal.style.display = 'block'; }); // Import button importBtn.addEventListener('click', function () { importModal.style.display = 'block'; }); // Close create modal createModal.querySelector('.close').addEventListener('click', function () { createModal.style.display = 'none'; }); createModal.querySelector('.cancel-create').addEventListener('click', function () { createModal.style.display = 'none'; }); // Close import modal importModal.querySelector('.close').addEventListener('click', function () { importModal.style.display = 'none'; }); importModal.querySelector('.cancel-import').addEventListener('click', function () { importModal.style.display = 'none'; }); // Close edit modal editModal.querySelector('.close').addEventListener('click', function () { editModal.style.display = 'none'; }); editModal.querySelector('.cancel-edit').addEventListener('click', function () { editModal.style.display = 'none'; }); // File name display const importFile = document.getElementById('importFile'); const fileName = document.getElementById('fileName'); importFile.addEventListener('change', function () { if (this.files.length > 0) { fileName.textContent = this.files[0].name; } else { fileName.textContent = '选择文件...'; } }); // Customer filter change event customerFilter.addEventListener('change', function () { selectedCustomerFilter = this.value; currentPage = 1; applyAllCustomerFilters(); }); if (customerSearchInput) { customerSearchInput.addEventListener('input', function () { customerSearchQuery = (this.value || '').trim(); if (currentSection === 'customer') { currentPage = 1; applyAllCustomerFilters(); } }); } // Apply date filter for dashboard document.getElementById('applyFilters').addEventListener('click', function () { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; applyDateFilter(startDate, endDate); }); // Chart title change events document.getElementById('statusChartTitle').addEventListener('input', function () { applyDateFilter(document.getElementById('startDate').value, document.getElementById('endDate').value); }); document.getElementById('typeChartTitle').addEventListener('input', function () { applyDateFilter(document.getElementById('startDate').value, document.getElementById('endDate').value); }); // Switch between sections function switchSection(section) { navItems.forEach(item => { item.classList.remove('active'); }); if (section === 'customer') { customerSection.classList.add('active'); dashboardSection.classList.remove('active'); document.querySelector('[data-section="customer"]').classList.add('active'); pageTitle.textContent = '客户管理'; currentSection = 'customer'; loadCustomers(); } else if (section === 'dashboard') { customerSection.classList.remove('active'); dashboardSection.classList.add('active'); document.querySelector('[data-section="dashboard"]').classList.add('active'); pageTitle.textContent = '数据仪表板'; currentSection = 'dashboard'; loadDashboardData(); } // Close sidebar on mobile after navigation if (window.innerWidth <= 768) { sidebar.classList.remove('open'); } } // Load customers from API async function loadCustomers() { try { const response = await authenticatedFetch('/api/customers?page=1&pageSize=1000'); const data = await response.json(); if (data.customers) { allCustomers = data.customers; populateCustomerFilter(); applyAllCustomerFilters(); } } catch (error) { console.error('Error loading customers:', error); } } // Populate customer filter dropdown function populateCustomerFilter() { console.log('Populating filter, allCustomers:', allCustomers); const uniqueCustomers = [...new Set(allCustomers.map(c => c.intendedProduct).filter(c => c))]; console.log('Unique customers:', uniqueCustomers); customerFilter.innerHTML = ''; uniqueCustomers.forEach(customer => { const option = document.createElement('option'); option.value = customer; option.textContent = customer; customerFilter.appendChild(option); }); if (selectedCustomerFilter) { customerFilter.value = selectedCustomerFilter; } } // Filter customers by selected customer function filterCustomers(selectedCustomer) { selectedCustomerFilter = selectedCustomer; currentPage = 1; applyAllCustomerFilters(); } function customerMatchesQuery(customer, query) { if (!query) return true; const fields = [ customer.customerName, customer.intendedProduct, customer.version, customer.description, customer.solution, customer.type, customer.module, customer.statusProgress, customer.reporter ]; const haystack = fields .map(v => String(v || '')) .join(' ') .toLowerCase(); const normalizedHaystack = haystack.replace(/[\/.]/g, '-'); const needle = query.toLowerCase(); const normalizedNeedle = needle.replace(/[\/.]/g, '-'); return normalizedHaystack.includes(normalizedNeedle); } function applyAllCustomerFilters() { let next = [...allCustomers]; if (selectedCustomerFilter) { next = next.filter(c => c.intendedProduct === selectedCustomerFilter); } if (customerSearchQuery) { next = next.filter(c => customerMatchesQuery(c, customerSearchQuery)); } filteredCustomers = next; applyCustomerFilter(); } function toCsvCell(value) { const raw = String(value ?? ''); const normalized = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); const escaped = normalized.replace(/"/g, '""'); return `"${escaped}"`; } function downloadCsv(filename, rows) { const content = ['\uFEFF' + rows[0], ...rows.slice(1)].join('\r\n'); const blob = new Blob([content], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } function exportCustomersToCsv(customers) { const header = [ '客户', '版本', '描述', '解决方案', '类型', '模块', '状态与进度', '报告人', '时间' ].map(toCsvCell).join(','); const lines = customers.map(c => { const date = normalizeDateValue(c.customerName) || (c.customerName || ''); const cells = [ c.intendedProduct || '', c.version || '', c.description || '', c.solution || '', c.type || '', c.module || '', c.statusProgress || '', c.reporter || '', date ]; return cells.map(toCsvCell).join(','); }); const now = new Date(); const y = now.getFullYear(); const m = String(now.getMonth() + 1).padStart(2, '0'); const d = String(now.getDate()).padStart(2, '0'); const hh = String(now.getHours()).padStart(2, '0'); const mm = String(now.getMinutes()).padStart(2, '0'); const ss = String(now.getSeconds()).padStart(2, '0'); const filename = `客户列表_${y}${m}${d}_${hh}${mm}${ss}.csv`; downloadCsv(filename, [header, ...lines]); } if (refreshCustomersBtn) { refreshCustomersBtn.addEventListener('click', () => { if (currentSection === 'customer') { loadCustomers(); } }); } if (exportCustomersBtn) { exportCustomersBtn.addEventListener('click', () => { if (currentSection !== 'customer') return; exportCustomersToCsv(filteredCustomers); }); } // Apply customer filter and render table with pagination function applyCustomerFilter() { totalItems = filteredCustomers.length; totalPages = Math.ceil(totalItems / pageSize); const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedCustomers = filteredCustomers.slice(startIndex, endIndex); renderCustomerTable(paginatedCustomers); updatePaginationControls(); } // Load all customers for dashboard async function loadAllCustomers() { console.log('loadAllCustomers called'); try { const response = await authenticatedFetch('/api/customers?page=1&pageSize=1000'); const data = await response.json(); console.log('Dashboard data received:', data); if (data.customers) { dashboardCustomers = data.customers; console.log('Dashboard customers set:', dashboardCustomers.length, 'customers'); updateDashboardStats(dashboardCustomers); renderStatusChart(dashboardCustomers); renderTypeChart(dashboardCustomers); } else { console.error('No customers in dashboard data'); } } catch (error) { console.error('Error loading all customers:', error); } } // Apply date filter for dashboard function applyDateFilter(startDate, endDate) { let filteredData = dashboardCustomers; if (startDate) { filteredData = filteredData.filter(c => { if (!c.customerName) return false; const date = normalizeDateValue(c.customerName); return date && date >= startDate; }); } if (endDate) { filteredData = filteredData.filter(c => { if (!c.customerName) return false; const date = normalizeDateValue(c.customerName); return date && date <= endDate; }); } updateDashboardStats(filteredData); renderStatusChart(filteredData); renderTypeChart(filteredData); } // Render customer table function renderCustomerTable(customers) { customerTableBody.innerHTML = ''; customers.forEach(customer => { const row = document.createElement('tr'); const date = customer.customerName || ''; const fields = [ { value: customer.intendedProduct || '', name: 'intendedProduct' }, { value: customer.version || '', name: 'version' }, { value: customer.description || '', name: 'description' }, { value: customer.solution || '', name: 'solution' }, { value: customer.type || '', name: 'type' }, { value: customer.module || '', name: 'module' }, { value: customer.statusProgress || '', name: 'statusProgress' }, { value: customer.reporter || '', name: 'reporter' }, { value: date, name: 'date' } ]; fields.forEach(field => { const td = document.createElement('td'); const textValue = String(field.value ?? ''); td.textContent = textValue; td.setAttribute('data-tooltip', textValue); if (field.name === 'description' || field.name === 'solution') { td.classList.add('overflow-cell'); } row.appendChild(td); }); const actionTd = document.createElement('td'); actionTd.innerHTML = ` `; row.appendChild(actionTd); customerTableBody.appendChild(row); }); checkTextOverflow(); document.querySelectorAll('.edit-btn').forEach(btn => { btn.addEventListener('click', function () { const customerId = this.getAttribute('data-id'); openEditModal(customerId); }); }); document.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', function () { const customerId = this.getAttribute('data-id'); deleteCustomer(customerId); }); }); } function checkTextOverflow() { // Use requestAnimationFrame to ensure DOM is fully rendered before checking overflow requestAnimationFrame(() => { const cells = customerTableBody.querySelectorAll('td[data-tooltip]'); cells.forEach(cell => { const cellText = (cell.getAttribute('data-tooltip') || '').trim(); const shouldAlwaysShowTooltip = cell.classList.contains('overflow-cell') && cellText.length > 0; const hasOverflow = cell.scrollWidth > cell.clientWidth; if (shouldAlwaysShowTooltip || hasOverflow) { cell.classList.add('has-overflow'); } else { cell.classList.remove('has-overflow'); } }); }); } // Update pagination controls function updatePaginationControls() { const startItem = totalItems === 0 ? 0 : (currentPage - 1) * pageSize + 1; const endItem = Math.min(currentPage * pageSize, totalItems); document.getElementById('paginationInfo').textContent = `显示 ${startItem}-${endItem} 共 ${totalItems} 条`; document.getElementById('firstPage').disabled = currentPage === 1; document.getElementById('prevPage').disabled = currentPage === 1; document.getElementById('nextPage').disabled = currentPage === totalPages; document.getElementById('lastPage').disabled = currentPage === totalPages; const pageNumbers = document.getElementById('pageNumbers'); pageNumbers.innerHTML = ''; const maxVisiblePages = 5; let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)); let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); if (endPage - startPage + 1 < maxVisiblePages) { startPage = Math.max(1, endPage - maxVisiblePages + 1); } for (let i = startPage; i <= endPage; i++) { const pageBtn = document.createElement('button'); pageBtn.className = `page-number ${i === currentPage ? 'active' : ''}`; pageBtn.textContent = i; pageBtn.addEventListener('click', () => { currentPage = i; loadCustomers(); }); pageNumbers.appendChild(pageBtn); } } // Create customer createCustomerForm.addEventListener('submit', async function (e) { e.preventDefault(); const formData = { customerName: document.getElementById('customerName').value, intendedProduct: document.getElementById('intendedProduct').value, version: document.getElementById('createVersion').value, description: document.getElementById('createDescription').value, solution: document.getElementById('createSolution').value, type: document.getElementById('createType').value, module: document.getElementById('createModule').value, statusProgress: document.getElementById('createStatusProgress').value, reporter: document.getElementById('createReporter').value }; try { const response = await authenticatedFetch('/api/customers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); if (response.ok) { createCustomerForm.reset(); createModal.style.display = 'none'; loadCustomers(); alert('客户创建成功!'); } else { alert('创建客户时出错'); } } catch (error) { console.error('Error creating customer:', error); alert('创建客户时出错'); } }); // Import customers importFileForm.addEventListener('submit', async function (e) { e.preventDefault(); const formData = new FormData(); const fileInput = document.getElementById('importFile'); if (!fileInput.files.length) { alert('请选择要导入的文件'); return; } formData.append('file', fileInput.files[0]); try { const response = await authenticatedFetch('/api/customers/import', { method: 'POST', body: formData }); if (response.ok) { const result = await response.json(); const message = `客户导入成功!\n导入数量: ${result.importedCount}\n重复数量: ${result.duplicateCount}`; alert(message); importFileForm.reset(); fileName.textContent = '选择文件...'; importModal.style.display = 'none'; loadCustomers(); } else { alert('导入客户时出错'); } } catch (error) { console.error('Error importing customers:', error); alert('导入客户时出错'); } }); // Open edit modal async function openEditModal(customerId) { try { const response = await authenticatedFetch(`/api/customers/${customerId}`); const customer = await response.json(); document.getElementById('editCustomerId').value = customer.id; document.getElementById('editCustomerName').value = normalizeDateValue(customer.customerName); document.getElementById('editIntendedProduct').value = customer.intendedProduct || ''; document.getElementById('editVersion').value = customer.version || ''; document.getElementById('editDescription').value = customer.description || ''; document.getElementById('editSolution').value = customer.solution || ''; document.getElementById('editType').value = customer.type || ''; document.getElementById('editModule').value = customer.module || ''; document.getElementById('editStatusProgress').value = customer.statusProgress || ''; document.getElementById('editReporter').value = customer.reporter || ''; editModal.style.display = 'block'; } catch (error) { console.error('Error loading customer for edit:', error); } } window.addEventListener('click', function (e) { if (e.target === createModal) { createModal.style.display = 'none'; } if (e.target === importModal) { importModal.style.display = 'none'; } if (e.target === editModal) { editModal.style.display = 'none'; } }); // Update customer editCustomerForm.addEventListener('submit', async function (e) { e.preventDefault(); const customerId = document.getElementById('editCustomerId').value; const formData = { customerName: document.getElementById('editCustomerName').value, intendedProduct: document.getElementById('editIntendedProduct').value, version: document.getElementById('editVersion').value, description: document.getElementById('editDescription').value, solution: document.getElementById('editSolution').value, type: document.getElementById('editType').value, module: document.getElementById('editModule').value, statusProgress: document.getElementById('editStatusProgress').value, reporter: document.getElementById('editReporter').value }; try { const response = await authenticatedFetch(`/api/customers/${customerId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); if (response.ok) { editModal.style.display = 'none'; loadCustomers(); alert('客户更新成功!'); } else { alert('更新客户时出错'); } } catch (error) { console.error('Error updating customer:', error); alert('更新客户时出错'); } }); // Delete customer async function deleteCustomer(customerId) { if (!confirm('确定要删除这个客户吗?')) { return; } try { const response = await authenticatedFetch(`/api/customers/${customerId}`, { method: 'DELETE' }); if (response.ok) { loadCustomers(); alert('客户删除成功!'); } else { alert('删除客户时出错'); } } catch (error) { console.error('Error deleting customer:', error); alert('删除客户时出错'); } } // Dashboard functionality async function loadDashboardData() { console.log('loadDashboardData called'); await loadAllCustomers(); } // Update dashboard statistics function updateDashboardStats(customers) { const totalCustomers = new Set(customers.map(c => c.intendedProduct).filter(c => c)).size; const now = new Date(); const currentMonth = String(now.getMonth() + 1).padStart(2, '0'); const currentYear = now.getFullYear(); const newCustomers = new Set( customers.filter(customer => { if (!customer.customerName) return false; const normalized = normalizeDateValue(customer.customerName); if (!normalized) return false; const year = parseInt(normalized.slice(0, 4), 10); const month = normalized.slice(5, 7); return year === currentYear && month === currentMonth; }).map(c => c.intendedProduct).filter(c => c) ).size; const products = new Set(customers.map(c => c.intendedProduct)).size; const completed = new Set( customers.filter(c => c.statusProgress && (c.statusProgress.includes('已修复') || c.statusProgress.includes('完成') || c.statusProgress.toLowerCase().includes('complete')) ).map(c => c.intendedProduct).filter(c => c) ).size; document.getElementById('totalCustomers').textContent = totalCustomers; document.getElementById('newCustomers').textContent = newCustomers; document.getElementById('totalProducts').textContent = products; document.getElementById('completedTasks').textContent = completed; } function renderStatusChart(customers) { const ctx = document.getElementById('statusChart').getContext('2d'); const selectedField = document.getElementById('chartFieldSelect').value; const chartTitle = document.getElementById('statusChartTitle').value || '数据分布'; const fieldCount = {}; customers.forEach(customer => { const value = customer[selectedField] || '未设置'; fieldCount[value] = (fieldCount[value] || 0) + 1; }); const labels = Object.keys(fieldCount); const data = Object.values(fieldCount); if (statusChartInstance) { statusChartInstance.destroy(); } statusChartInstance = new Chart(ctx, { type: 'pie', data: { labels: labels, datasets: [{ data: data, backgroundColor: [ '#FF6B35', '#F28C28', '#333', '#4CAF50', '#2196F3', '#FFC107', '#9C27B0', '#00BCD4', '#8BC34A', '#FF5722' ] }] }, options: { responsive: true, plugins: { legend: { position: 'bottom' }, title: { display: true, text: chartTitle, font: { size: 16, weight: 'bold' } } } } }); } // Render customer type chart function renderTypeChart(customers) { console.log('renderTypeChart called with customers:', customers); const canvas = document.getElementById('typeChart'); console.log('typeChart canvas element:', canvas); if (!canvas) { console.error('typeChart canvas not found'); return; } const ctx = canvas.getContext('2d'); const chartTitle = document.getElementById('typeChartTitle').value || '客户类型'; if (typeChartInstance) { typeChartInstance.destroy(); } const selectedField = document.getElementById('typeChartFieldSelect').value; const typeCount = {}; customers.forEach(customer => { const type = customer[selectedField] || '未设置'; typeCount[type] = (typeCount[type] || 0) + 1; }); const labels = Object.keys(typeCount); const data = Object.values(typeCount); console.log('Type chart labels:', labels); console.log('Type chart data:', data); typeChartInstance = new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ data: data, backgroundColor: [ '#333', '#FF6B35', '#F28C28', '#4CAF50', '#2196F3', '#FFC107' ] }] }, options: { responsive: true, plugins: { legend: { position: 'bottom' }, title: { display: true, text: chartTitle, font: { size: 16, weight: 'bold' } } } } }); console.log('Type chart created successfully'); } // Initialize the app loadCustomers().then(() => { loadAllCustomers(); }); // Pagination event listeners document.getElementById('firstPage').addEventListener('click', () => { if (currentPage > 1) { currentPage = 1; applyCustomerFilter(); } }); document.getElementById('prevPage').addEventListener('click', () => { if (currentPage > 1) { currentPage--; applyCustomerFilter(); } }); document.getElementById('nextPage').addEventListener('click', () => { if (currentPage < totalPages) { currentPage++; applyCustomerFilter(); } }); document.getElementById('lastPage').addEventListener('click', () => { if (currentPage < totalPages) { currentPage = totalPages; applyCustomerFilter(); } }); document.getElementById('pageSizeSelect').addEventListener('change', (e) => { pageSize = parseInt(e.target.value); currentPage = 1; loadCustomers(); }); // Chart field select event listener document.getElementById('chartFieldSelect').addEventListener('change', function () { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; applyDateFilter(startDate, endDate); }); // Type chart field select event listener document.getElementById('typeChartFieldSelect').addEventListener('change', function () { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; applyDateFilter(startDate, endDate); }); // 登出功能 const logoutBtn = document.createElement('button'); logoutBtn.className = 'icon-btn'; logoutBtn.innerHTML = ''; logoutBtn.title = '登出'; logoutBtn.addEventListener('click', () => { if (confirm('确定要退出登录吗?')) { localStorage.removeItem('crmToken'); window.location.href = '/static/login.html'; } }); const headerRight = document.querySelector('.header-right'); if (headerRight) { headerRight.appendChild(logoutBtn); } });