看了下Javascript相关的代码,感觉像是把一部分的逻辑处理放到了前端里面。问了下Gemini,发现这个确实可以写后端,Node.js就是。不过说回我们这个代码,为什么增删改查一类的不能直接放到python代码里,而是需要Javascript在前端处理?这就是上一章最后说到的问题,那就是如果把这些功能完全放到后端,那么每次运行一个简单的功能,整个网页都会重新载入,很费劲,而Javascript可以在不刷新页面的情况下,局部处理一些简单的功能。
因为之前我们在后端已经写好了客户的增删改查,所以这一章主要是前端Javascript相关的编写。因为不是学习重点,简单看了下了解下就算了。
{% extends "customer/base.html" %}
{% block title %}我的客户 - 喵喵CRM{% endblock %}
{% block content %}
<style>
/* --- Modal 样式 --- */
/* 弹窗背景遮罩 */
.modal-overlay {
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: 25px;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
width: 400px;
max-width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
padding-bottom: 15px;
}
.modal-header h3 {
margin: 0;
}
/* 关闭按钮 */
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #888;
}
/* 表单样式 */
.modal-form-group {
margin-bottom: 15px;
}
.modal-form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.modal-form-group input {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box; /* 确保 padding 不会影响宽度 */
}
.modal-actions {
text-align: right;
margin-top: 20px;
}
.action-btn {
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-left: 10px;
}
.save-btn {
background-color: #28a745;
color: white;
}
.cancel-btn {
background-color: #6c757d;
color: white;
}
/* --- 表格按钮样式 --- */
.table-action-btn {
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-right: 5px;
}
.edit-btn {
background-color: #ffc107;
color: #333;
}
.delete-btn {
background-color: #dc3545;
color: white;
}
</style>
<div style="max-width: 900px; margin: 0 auto; padding: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">我的客户列表</h2>
<button id="add-customer-btn" style="padding: 10px 15px; background-color: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px;">
+ 新增客户
</button>
</div>
<table style="width: 100%; border-collapse: collapse;">
<thead style="background-color: #333; color: white;">
<tr>
<th style="padding: 12px; text-align: left;">姓名</th>
<th style="padding: 12px; text-align: left;">邮箱</th>
<th style="padding: 12px; text-align: left;">电话</th>
<th style="padding: 12px; text-align: left;">地址</th>
<th style="padding: 12px; text-align: left;">负责人</th>
<th style="padding: 12px; text-align: left; width: 120px;">操作</th>
</tr>
</thead>
<tbody id="customer-table-body">
{% for customer in customers %}
<tr style="border-bottom: 1px solid #ddd;" data-id="{{ customer.id }}">
<td style="padding: 12px;" data-field="name">{{ customer.name }}</td>
<td style="padding: 12px;" data-field="email">{{ customer.email|default:'--' }}</td>
<td style="padding: 12px;" data-field="phone">{{ customer.phone|default:'--' }}</td>
<td style="padding: 12px;" data-field="address">{{ customer.address|default:'--' }}</td>
<td style="padding: 12px;" data-field="owner">{{ customer.owner }}</td>
<td style="padding: 12px;">
<button class="table-action-btn edit-btn">编辑</button>
<button class="table-action-btn delete-btn">删除</button>
</td>
</tr>
{% empty %}
<tr id="empty-row">
<td colspan="5" style="text-align: center; padding: 20px; color: #777;">
您还没有客户,快去添加第一个客户吧!
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div id="customer-modal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">新增客户</h3>
<button id="close-modal-btn" class="close-btn">×</button>
</div>
<form id="modal-form">
<input type="hidden" id="customer-id-input">
<div class="modal-form-group">
<label for="name-input">姓名</label>
<input type="text" id="name-input" required>
</div>
<div class="modal-form-group">
<label for="phone-input">电话 (可选)</label>
<input type="tel" id="phone-input">
</div>
<div class="modal-form-group">
<label for="email-input">邮箱 (可选)</label>
<input type="email" id="email-input">
</div>
<div class="modal-form-group">
<label for="address-input">地址 (可选)</label>
<input type="text" id="address-input">
</div>
<div class="modal-actions">
<button type="button" class="action-btn cancel-btn">取消</button>
<button type="submit" class="action-btn save-btn">保存</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// --- CSRF Token 获取函数 ---
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
// --- 获取所有需要的DOM元素 ---
const addBtn = document.getElementById('add-customer-btn');
const customerTableBody = document.getElementById('customer-table-body');
const modal = document.getElementById('customer-modal');
const modalForm = document.getElementById('modal-form');
const modalTitle = document.getElementById('modal-title');
const customerIdInput = document.getElementById('customer-id-input');
const nameInput = document.getElementById('name-input');
const phoneInput = document.getElementById('phone-input');
const emailInput = document.getElementById('email-input');
const addressInput = document.getElementById('address-input');
// --- 弹窗控制函数 ---
function openModal() {
modal.style.display = 'flex';
}
function closeModal() {
modal.style.display = 'none';
modalForm.reset(); // 清空表单
customerIdInput.value = ''; // 清空隐藏的ID
}
// 弹窗关闭按钮 和 取消按钮
modal.querySelector('#close-modal-btn').addEventListener('click', closeModal);
modal.querySelector('.cancel-btn').addEventListener('click', closeModal);
// 点击遮罩区域关闭弹窗
modal.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
// --- 事件监听:点击“新增客户”按钮 ---
addBtn.addEventListener('click', function() {
modalTitle.textContent = '新增客户';
openModal();
});
// --- 表单提交处理(新增或编辑)---
modalForm.addEventListener('submit', function(event) {
event.preventDefault(); // 阻止表单默认的刷新页面的提交行为
const customerId = customerIdInput.value;
const isEditing = !!customerId; // 如果customerId有值,则为编辑模式
const url = isEditing ? `/api/customers/${customerId}/` : '/api/customers/';
const method = isEditing ? 'PATCH' : 'POST';
const formData = {
name: nameInput.value,
phone: phoneInput.value,
email: emailInput.value,
address: addressInput.value,
};
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken,
},
body: JSON.stringify(formData),
})
.then(response => {
if (!response.ok) throw new Error('请求失败');
return response.json();
})
.then(data => {
if (isEditing) {
// 更新表格中的行
const row = customerTableBody.querySelector(`tr[data-id='${customerId}']`);
row.querySelector('[data-field="name"]').textContent = data.name;
row.querySelector('[data-field="email"]').textContent = data.email || '--';
row.querySelector('[data-field="address"]').textContent = data.address || '--';
row.querySelector('[data-field="phone"]').textContent = data.phone || '--';
alert('客户更新成功!');
} else {
// 在表格中添加新行
const emptyRow = document.getElementById('empty-row');
if(emptyRow) emptyRow.remove(); // 如果存在“空数据”提示,先移除
const newRowHtml = `
<tr style="border-bottom: 1px solid #ddd;" data-id="${data.id}">
<td style="padding: 12px;" data-field="name">${data.name}</td>
<td style="padding: 12px;" data-field="email">${data.email || '--'}</td>
<td style="padding: 12px;" data-field="phone">${data.phone || '--'}</td>
<td style="padding: 12px;" data-field="address">${data.address || '--'}</td>
<td style="padding: 12px;" data-field="owner">${data.owner}</td>
<td style="padding: 12px;">
<button class="table-action-btn edit-btn">编辑</button>
<button class="table-action-btn delete-btn">删除</button>
</td>
</tr>
`;
customerTableBody.insertAdjacentHTML('beforeend', newRowHtml);
alert('客户添加成功!');
}
closeModal();
})
.catch(error => {
console.error('操作失败:', error);
alert('操作失败,请检查控制台输出。');
});
});
// --- 事件委托:处理表格中的点击事件 ---
customerTableBody.addEventListener('click', function(event) {
const target = event.target;
const customerRow = target.closest('tr');
if (!customerRow || !customerRow.dataset.id) return;
const customerId = customerRow.dataset.id;
// --- 处理“编辑”逻辑 ---
if (target.classList.contains('edit-btn')) {
modalTitle.textContent = '编辑客户';
// 从表格行中获取当前数据并填充到表单
customerIdInput.value = customerId;
nameInput.value = customerRow.querySelector('[data-field="name"]').textContent;
phoneInput.value = customerRow.querySelector('[data-field="phone"]').textContent;
emailInput.value = customerRow.querySelector('[data-field="email"]').textContent.replace('--', '');
addressInput.value = customerRow.querySelector('[data-field="address"]').textContent;
openModal()
}
// --- 处理“删除”逻辑 ---
if (target.classList.contains('delete-btn')) {
if (confirm('确定要删除这位客户吗?')) {
fetch(`/api/customers/${customerId}/`, {
method: 'DELETE',
headers: { 'X-CSRFToken': csrftoken },
})
.then(response => {
if (response.ok) {
customerRow.remove();
alert('删除成功!');
if (customerTableBody.getElementsByTagName('tr').length === 0) {
const emptyRowHtml = `<tr id="empty-row"><td colspan="5" style="text-align: center; padding: 20px; color: #777;">您还没有客户,快去添加第一个客户吧!</td></tr>`;
customerTableBody.innerHTML = emptyRowHtml;
}
} else {
alert('删除失败!');
}
})
.catch(error => console.error('删除请求失败:', error));
}
}
});
});
</script>
{% endblock %}