feat(联系人): 添加联系人信息导出为PDF功能

在联系人列表页面添加导出为PDF按钮,实现将筛选后的联系人信息导出为PDF文件。新增视图函数处理PDF生成逻辑,支持中文显示并保留原有筛选条件。
This commit is contained in:
2026-01-12 15:48:51 +08:00
parent 7c2095a52e
commit 8eba3f0160
6 changed files with 448 additions and 335 deletions

Binary file not shown.

View File

@@ -1,336 +1,345 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container mx-auto p-4"> <div class="container mx-auto p-4">
<h2 class="text-xl font-bold mb-4">联系人信息</h2> <h2 class="text-xl font-bold mb-4">联系人信息</h2>
<!-- 新增筛选表单 --> <!-- 新增筛选表单 -->
<form method="GET" class="mb-4 p-4 bg-gray-100 rounded-lg" id="filterForm"> <form method="GET" class="mb-4 p-4 bg-gray-100 rounded-lg" id="filterForm">
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
<!-- 分支机构筛选(带搜索的多选下拉框) --> <!-- 分支机构筛选(带搜索的多选下拉框) -->
<div class="flex flex-col w-full md:w-1/2"> <div class="flex flex-col w-full md:w-1/2">
<label class="text-sm font-medium">分支机构(可多选)</label> <label class="text-sm font-medium">分支机构(可多选)</label>
<div class="relative" id="branchSelectContainer"> <div class="relative" id="branchSelectContainer">
<!-- 选择框主体 --> <!-- 选择框主体 -->
<div class="relative"> <div class="relative">
<input <input
type="text" type="text"
id="branchSearchInput" id="branchSearchInput"
class="w-full pl-4 pr-10 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-300 focus:border-blue-500 outline-none text-sm" class="w-full pl-4 pr-10 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-300 focus:border-blue-500 outline-none text-sm"
placeholder="点击选择分支机构(可多选)..." placeholder="点击选择分支机构(可多选)..."
readonly readonly
onclick="toggleBranchDropdown()" onclick="toggleBranchDropdown()"
> >
<span class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 transition-transform duration-200" id="branchArrowIcon"> <span class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 transition-transform duration-200" id="branchArrowIcon">
<i class="fa fa-chevron-down"></i> <i class="fa fa-chevron-down"></i>
</span> </span>
<!-- 选中的机构显示 --> <!-- 选中的机构显示 -->
<div id="selectedBranches" class="absolute z-5 mt-1 w-full bg-white rounded-lg shadow-md hidden max-h-32 overflow-y-auto"> <div id="selectedBranches" class="absolute z-5 mt-1 w-full bg-white rounded-lg shadow-md hidden max-h-32 overflow-y-auto">
<div class="p-2"> <div class="p-2">
<div class="flex flex-wrap gap-1" id="branchTags"></div> <div class="flex flex-wrap gap-1" id="branchTags"></div>
</div> </div>
</div> </div>
</div> </div>
<!-- 下拉内容 --> <!-- 下拉内容 -->
<div id="branchDropdownContent" class="absolute z-10 mt-1 w-full bg-white rounded-lg shadow-md overflow-hidden hidden"> <div id="branchDropdownContent" class="absolute z-10 mt-1 w-full bg-white rounded-lg shadow-md overflow-hidden hidden">
<!-- 搜索输入框 --> <!-- 搜索输入框 -->
<div class="p-2 border-b border-gray-100"> <div class="p-2 border-b border-gray-100">
<input <input
type="text" type="text"
id="branchFilterInput" id="branchFilterInput"
class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-1 focus:ring-blue-300 focus:border-blue-500 outline-none text-sm" class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-1 focus:ring-blue-300 focus:border-blue-500 outline-none text-sm"
placeholder="搜索分支机构..." placeholder="搜索分支机构..."
oninput="filterBranchOptions()" oninput="filterBranchOptions()"
autofocus autofocus
> >
</div> </div>
<!-- 选项列表 --> <!-- 选项列表 -->
<div id="branchOptionsList" class="max-h-60 overflow-y-auto"> <div id="branchOptionsList" class="max-h-60 overflow-y-auto">
<ul> <ul>
{% for branch in branches %} {% for branch in branches %}
<li class="branch-option px-4 py-2.5 hover:bg-gray-100 cursor-pointer text-sm transition-colors flex items-center" <li class="branch-option px-4 py-2.5 hover:bg-gray-100 cursor-pointer text-sm transition-colors flex items-center"
data-id="{{ branch.id }}" data-id="{{ branch.id }}"
data-name="{{ branch.name }}"> data-name="{{ branch.name }}">
<input type="checkbox" class="mr-2 branch-checkbox" value="{{ branch.id }}"> <input type="checkbox" class="mr-2 branch-checkbox" value="{{ branch.id }}">
{{ branch.name }} {{ branch.name }}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="p-2 border-t border-gray-100 flex gap-2"> <div class="p-2 border-t border-gray-100 flex gap-2">
<button type="button" class="flex-1 px-3 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600" onclick="selectAllBranches()">全选</button> <button type="button" class="flex-1 px-3 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600" onclick="selectAllBranches()">全选</button>
<button type="button" class="flex-1 px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600" onclick="clearAllBranches()">清空</button> <button type="button" class="flex-1 px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600" onclick="clearAllBranches()">清空</button>
</div> </div>
<!-- 独立的清空全部选择按钮 --> <!-- 独立的清空全部选择按钮 -->
<div class="p-2 border-t border-gray-100"> <div class="p-2 border-t border-gray-100">
<button type="button" class="w-full px-3 py-2 text-sm bg-orange-500 text-white rounded-lg hover:bg-orange-600 font-medium" onclick="clearAllBranches()" title="清空所有机构选择"> <button type="button" class="w-full px-3 py-2 text-sm bg-orange-500 text-white rounded-lg hover:bg-orange-600 font-medium" onclick="clearAllBranches()" title="清空所有机构选择">
<i class="fa fa-refresh mr-1"></i>清空全部选择 <i class="fa fa-refresh mr-1"></i>清空全部选择
</button> </button>
</div> </div>
<!-- 无结果提示 --> <!-- 无结果提示 -->
<div id="branchNoResult" class="px-4 py-4 text-center text-gray-500 text-sm hidden"> <div id="branchNoResult" class="px-4 py-4 text-center text-gray-500 text-sm hidden">
没有找到匹配的分支机构 没有找到匹配的分支机构
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 联系人类别筛选(保持原结构) --> <!-- 联系人类别筛选(保持原结构) -->
<div class="flex flex-col w-full md:w-1/3"> <div class="flex flex-col w-full md:w-1/3">
<label class="text-sm font-medium">联系人类别</label> <label class="text-sm font-medium">联系人类别</label>
<input type="text" name="category" class="mt-1 p-2 border rounded" placeholder="输入类别名称筛选" value="{{ selected_category|default:'' }}"> <input type="text" name="category" class="mt-1 p-2 border rounded" placeholder="输入类别名称筛选" value="{{ selected_category|default:'' }}">
</div> </div>
<!-- 联系人姓名筛选(保持原结构) --> <!-- 联系人姓名筛选(保持原结构) -->
<div class="flex flex-col w-full md:w-1/3"> <div class="flex flex-col w-full md:w-1/3">
<label class="text-sm font-medium">联系人姓名</label> <label class="text-sm font-medium">联系人姓名</label>
<input type="text" name="contact_name" class="mt-1 p-2 border rounded" placeholder="输入联系人姓名筛选" value="{{ selected_contact_name|default:'' }}"> <input type="text" name="contact_name" class="mt-1 p-2 border rounded" placeholder="输入联系人姓名筛选" value="{{ selected_contact_name|default:'' }}">
</div> </div>
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">筛选</button> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">筛选</button>
<button type="button" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors duration-200 font-medium" onclick="clearFilters()"> <button type="button" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors duration-200 font-medium" onclick="clearFilters()">
<i class="fa fa-times mr-1"></i>清空筛选 <i class="fa fa-times mr-1"></i>清空筛选
</button> </button>
</div> </div>
</div> </div>
<!-- 隐藏的分支机构ID输入框用于表单提交 --> <!-- 隐藏的分支机构ID输入框用于表单提交 -->
<input type="hidden" name="branches" id="selectedBranchesInput"> <input type="hidden" name="branches" id="selectedBranchesInput">
</form> </form>
<table class="min-w-full border-collapse border border-gray-300"> <table class="min-w-full border-collapse border border-gray-300">
<thead> <thead>
<tr> <tr>
<th class="p-2 border border-gray-300 bg-gray-100">分支机构</th> <th class="p-2 border border-gray-300 bg-gray-100">分支机构</th>
<th class="p-2 border border-gray-300 bg-gray-100">分类</th> <th class="p-2 border border-gray-300 bg-gray-100">分类</th>
<th class="p-2 border border-gray-300 bg-gray-100">姓名</th> <th class="p-2 border border-gray-300 bg-gray-100">姓名</th>
<th class="p-2 border border-gray-300 bg-gray-100">电话</th> <th class="p-2 border border-gray-300 bg-gray-100">电话</th>
<th class="p-2 border border-gray-300 bg-gray-100">邮箱</th> <th class="p-2 border border-gray-300 bg-gray-100">邮箱</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for contact in contacts %} {% for contact in contacts %}
<tr class="{% cycle 'bg-white' 'bg-gray-50' %}"> <tr class="{% cycle 'bg-white' 'bg-gray-50' %}">
<td class="p-2 border border-gray-300">{{ contact.branch.name }}</td> <td class="p-2 border border-gray-300">{{ contact.branch.name }}</td>
<td class="p-2 border border-gray-300">{{ contact.category|default:"" }}</td> <td class="p-2 border border-gray-300">{{ contact.category|default:"" }}</td>
<td class="p-2 border border-gray-300">{{ contact.name|default:"" }}</td> <td class="p-2 border border-gray-300">{{ contact.name|default:"" }}</td>
<td class="p-2 border border-gray-300">{{ contact.phone|default:"" }}</td> <td class="p-2 border border-gray-300">{{ contact.phone|default:"" }}</td>
<td class="p-2 border border-gray-300">{{ contact.email|default:"" }}</td> <td class="p-2 border border-gray-300">{{ contact.email|default:"" }}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="5" class="p-4 text-center text-gray-500">没有找到匹配的联系人</td> <td colspan="5" class="p-4 text-center text-gray-500">没有找到匹配的联系人</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
<div class="mt-4 flex justify-end">
<script> <a href="{% url 'export-contacts-pdf' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="inline-flex items-center px-4 py-2 bg-purple-600 hover:bg-purple-800 text-white font-medium rounded-lg shadow-md transition duration-300 ease-in-out transform hover:scale-105 hover:shadow-lg">
// 选中的分支机构 <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
let selectedBranches = new Set(); <path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
// 初始化页面 导出为PDF
document.addEventListener('DOMContentLoaded', function() { </a>
// 初始化选中的机构 </div>
initializeSelectedBranches(); </div>
// 绑定复选框事件 <script>
bindCheckboxEvents(); // 选中的分支机构
}); let selectedBranches = new Set();
// 初始化选中的机构(从表单参数中读取) // 初始化页面
function initializeSelectedBranches() { document.addEventListener('DOMContentLoaded', function() {
const urlParams = new URLSearchParams(window.location.search); // 初始化选中的机构
const branchesParam = urlParams.get('branches'); initializeSelectedBranches();
if (branchesParam) {
const branchIds = branchesParam.split(','); // 绑定复选框事件
branchIds.forEach(id => { bindCheckboxEvents();
selectedBranches.add(id); });
const checkbox = document.querySelector(`input[value="${id}"]`);
if (checkbox) { // 初始化选中的机构(从表单参数中读取)
checkbox.checked = true; function initializeSelectedBranches() {
} const urlParams = new URLSearchParams(window.location.search);
}); const branchesParam = urlParams.get('branches');
} if (branchesParam) {
updateBranchSelectionUI(); const branchIds = branchesParam.split(',');
} branchIds.forEach(id => {
selectedBranches.add(id);
// 绑定复选框事件 const checkbox = document.querySelector(`input[value="${id}"]`);
function bindCheckboxEvents() { if (checkbox) {
document.querySelectorAll('.branch-checkbox').forEach(checkbox => { checkbox.checked = true;
checkbox.addEventListener('change', function() { }
const branchId = this.value; });
const branchName = this.closest('.branch-option').dataset.name; }
updateBranchSelectionUI();
if (this.checked) { }
selectedBranches.add(branchId);
} else { // 绑定复选框事件
selectedBranches.delete(branchId); function bindCheckboxEvents() {
} document.querySelectorAll('.branch-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', function() {
updateBranchSelectionUI(); const branchId = this.value;
}); const branchName = this.closest('.branch-option').dataset.name;
});
} if (this.checked) {
selectedBranches.add(branchId);
// 更新机构选择UI } else {
function updateBranchSelectionUI() { selectedBranches.delete(branchId);
const selectedBranchesInput = document.getElementById('selectedBranchesInput'); }
const branchSearchInput = document.getElementById('branchSearchInput');
const selectedBranchesDiv = document.getElementById('selectedBranches'); updateBranchSelectionUI();
const branchTags = document.getElementById('branchTags'); });
});
// 更新隐藏输入框 }
if (selectedBranches.size > 0) {
selectedBranchesInput.value = Array.from(selectedBranches).join(','); // 更新机构选择UI
branchSearchInput.placeholder = `已选择 ${selectedBranches.size} 个机构`; function updateBranchSelectionUI() {
} else { const selectedBranchesInput = document.getElementById('selectedBranchesInput');
selectedBranchesInput.value = ''; const branchSearchInput = document.getElementById('branchSearchInput');
branchSearchInput.placeholder = '点击选择分支机构(可多选)...'; const selectedBranchesDiv = document.getElementById('selectedBranches');
} const branchTags = document.getElementById('branchTags');
// 更新显示的标签 // 更新隐藏输入框
branchTags.innerHTML = ''; if (selectedBranches.size > 0) {
selectedBranches.forEach(branchId => { selectedBranchesInput.value = Array.from(selectedBranches).join(',');
const branchName = document.querySelector(`input[value="${branchId}"]`).closest('.branch-option').dataset.name; branchSearchInput.placeholder = `已选择 ${selectedBranches.size} 个机构`;
const tag = document.createElement('span'); } else {
tag.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800'; selectedBranchesInput.value = '';
tag.innerHTML = ` branchSearchInput.placeholder = '点击选择分支机构(可多选)...';
${branchName} }
<button type="button" class="ml-1 text-blue-600 hover:text-blue-800" onclick="removeBranch('${branchId}')">
<i class="fa fa-times"></i> // 更新显示的标签
</button> branchTags.innerHTML = '';
`; selectedBranches.forEach(branchId => {
branchTags.appendChild(tag); const branchName = document.querySelector(`input[value="${branchId}"]`).closest('.branch-option').dataset.name;
}); const tag = document.createElement('span');
tag.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800';
// 控制选中机构显示区域 tag.innerHTML = `
if (selectedBranches.size > 0) { ${branchName}
selectedBranchesDiv.classList.remove('hidden'); <button type="button" class="ml-1 text-blue-600 hover:text-blue-800" onclick="removeBranch('${branchId}')">
} else { <i class="fa fa-times"></i>
selectedBranchesDiv.classList.add('hidden'); </button>
} `;
} branchTags.appendChild(tag);
});
// 移除机构
function removeBranch(branchId) { // 控制选中机构显示区域
selectedBranches.delete(branchId); if (selectedBranches.size > 0) {
const checkbox = document.querySelector(`input[value="${branchId}"]`); selectedBranchesDiv.classList.remove('hidden');
if (checkbox) { } else {
checkbox.checked = false; selectedBranchesDiv.classList.add('hidden');
} }
updateBranchSelectionUI(); }
}
// 移除机构
// 全选分支机构 function removeBranch(branchId) {
function selectAllBranches() { selectedBranches.delete(branchId);
selectedBranches.clear(); const checkbox = document.querySelector(`input[value="${branchId}"]`);
document.querySelectorAll('.branch-checkbox').forEach(checkbox => { if (checkbox) {
checkbox.checked = true; checkbox.checked = false;
selectedBranches.add(checkbox.value); }
}); updateBranchSelectionUI();
updateBranchSelectionUI(); }
}
// 全选分支机构
// 清空所有分支机构选择 function selectAllBranches() {
function clearAllBranches() { selectedBranches.clear();
selectedBranches.clear(); document.querySelectorAll('.branch-checkbox').forEach(checkbox => {
document.querySelectorAll('.branch-checkbox').forEach(checkbox => { checkbox.checked = true;
checkbox.checked = false; selectedBranches.add(checkbox.value);
}); });
updateBranchSelectionUI(); updateBranchSelectionUI();
} }
// 清空所有筛选 // 清空所有分支机构选择
function clearFilters() { function clearAllBranches() {
// 清空所有表单字段 selectedBranches.clear();
document.getElementById('filterForm').reset(); document.querySelectorAll('.branch-checkbox').forEach(checkbox => {
checkbox.checked = false;
// 清空机构选择 });
clearAllBranches(); updateBranchSelectionUI();
}
// 提交清空后的表单
window.location.href = window.location.pathname; // 清空所有筛选
} function clearFilters() {
// 清空所有表单字段
// 控制分支机构下拉框显示/隐藏 document.getElementById('filterForm').reset();
function toggleBranchDropdown() {
const dropdown = document.getElementById('branchDropdownContent'); // 清空机构选择
const arrow = document.getElementById('branchArrowIcon'); clearAllBranches();
if (dropdown.classList.contains('hidden')) {
dropdown.classList.remove('hidden'); // 提交清空后的表单
arrow.classList.add('rotate-180'); window.location.href = window.location.pathname;
document.getElementById('branchFilterInput').focus(); }
document.addEventListener('click', handleBranchOutsideClick);
} else { // 控制分支机构下拉框显示/隐藏
closeBranchDropdown(); function toggleBranchDropdown() {
} const dropdown = document.getElementById('branchDropdownContent');
} const arrow = document.getElementById('branchArrowIcon');
if (dropdown.classList.contains('hidden')) {
// 关闭分支机构下拉框 dropdown.classList.remove('hidden');
function closeBranchDropdown() { arrow.classList.add('rotate-180');
const dropdown = document.getElementById('branchDropdownContent'); document.getElementById('branchFilterInput').focus();
const arrow = document.getElementById('branchArrowIcon'); document.addEventListener('click', handleBranchOutsideClick);
dropdown.classList.add('hidden'); } else {
arrow.classList.remove('rotate-180'); closeBranchDropdown();
document.removeEventListener('click', handleBranchOutsideClick); }
} }
// 点击外部关闭下拉框 // 关闭分支机构下拉框
function handleBranchOutsideClick(event) { function closeBranchDropdown() {
const container = document.getElementById('branchSelectContainer'); const dropdown = document.getElementById('branchDropdownContent');
if (!container.contains(event.target)) { const arrow = document.getElementById('branchArrowIcon');
closeBranchDropdown(); dropdown.classList.add('hidden');
} arrow.classList.remove('rotate-180');
} document.removeEventListener('click', handleBranchOutsideClick);
}
// 筛选分支机构选项
function filterBranchOptions() { // 点击外部关闭下拉框
const searchText = document.getElementById('branchFilterInput').value.toLowerCase(); function handleBranchOutsideClick(event) {
const options = document.querySelectorAll('.branch-option'); const container = document.getElementById('branchSelectContainer');
const noResult = document.getElementById('branchNoResult'); if (!container.contains(event.target)) {
let hasMatch = false; closeBranchDropdown();
}
options.forEach(option => { }
const name = option.dataset.name.toLowerCase();
if (name.includes(searchText)) { // 筛选分支机构选项
option.style.display = 'block'; function filterBranchOptions() {
hasMatch = true; const searchText = document.getElementById('branchFilterInput').value.toLowerCase();
} else { const options = document.querySelectorAll('.branch-option');
option.style.display = 'none'; const noResult = document.getElementById('branchNoResult');
} let hasMatch = false;
});
options.forEach(option => {
// 显示或隐藏无结果提示 const name = option.dataset.name.toLowerCase();
if (searchText && !hasMatch) { if (name.includes(searchText)) {
noResult.classList.remove('hidden'); option.style.display = 'block';
} else { hasMatch = true;
noResult.classList.add('hidden'); } else {
} option.style.display = 'none';
} }
});
// 选择分支机构选项(保留原有功能以兼容单选操作)
document.querySelectorAll('.branch-option').forEach(option => { // 显示或隐藏无结果提示
option.addEventListener('click', function(e) { if (searchText && !hasMatch) {
// 如果点击的是复选框,不重复处理 noResult.classList.remove('hidden');
if (e.target.type === 'checkbox') { } else {
return; noResult.classList.add('hidden');
} }
}
const checkbox = this.querySelector('.branch-checkbox');
checkbox.checked = !checkbox.checked; // 选择分支机构选项(保留原有功能以兼容单选操作)
document.querySelectorAll('.branch-option').forEach(option => {
// 触发复选框的change事件 option.addEventListener('click', function(e) {
checkbox.dispatchEvent(new Event('change')); // 如果点击的是复选框,不重复处理
}); if (e.target.type === 'checkbox') {
}); return;
</script> }
const checkbox = this.querySelector('.branch-checkbox');
checkbox.checked = !checkbox.checked;
// 触发复选框的change事件
checkbox.dispatchEvent(new Event('change'));
});
});
</script>
{% endblock %} {% endblock %}

View File

@@ -15,6 +15,7 @@ urlpatterns = [
path('branch/info/', views.Branchinfo, name='branchinfo'), path('branch/info/', views.Branchinfo, name='branchinfo'),
path('statistics/', views.Statistics, name='statistics'), path('statistics/', views.Statistics, name='statistics'),
path('contact/', views.contact_list, name='contact-list'), path('contact/', views.contact_list, name='contact-list'),
path('contact/export/pdf/', views.export_contacts_pdf, name='export-contacts-pdf'),
path('equipment-images/', views.equipment_images, name='equipment-images'), path('equipment-images/', views.equipment_images, name='equipment-images'),
path('public-screens/', views.public_screens, name='public-screens'), path('public-screens/', views.public_screens, name='public-screens'),
path('video-terminals/', views.video_terminal_list, name='video-terminals'), path('video-terminals/', views.video_terminal_list, name='video-terminals'),

View File

@@ -466,6 +466,109 @@ def contact_list(request):
return render(request, 'contact_list.html', context) return render(request, 'contact_list.html', context)
def export_contacts_pdf(request):
# 创建响应对象
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="contacts_info.pdf"'
# 注册中文字体
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
# 尝试注册系统中文字体这里使用Windows系统自带的中文字体
try:
# 尝试注册微软雅黑字体
pdfmetrics.registerFont(TTFont('MSYaHei', 'C:/Windows/Fonts/msyh.ttc'))
chinese_font = 'MSYaHei'
except:
try:
# 如果微软雅黑不存在,尝试注册宋体
pdfmetrics.registerFont(TTFont('SimSun', 'C:/Windows/Fonts/simsun.ttc'))
chinese_font = 'SimSun'
except:
# 如果都找不到,使用默认字体(可能仍会有乱码)
chinese_font = 'Helvetica'
# 创建PDF文档
doc = SimpleDocTemplate(response, pagesize=letter)
elements = []
# 添加标题
styles = getSampleStyleSheet()
# 复制Title样式并修改字体
title_style = ParagraphStyle(
'ChineseTitle',
parent=styles['Title'],
fontName=chinese_font,
fontSize=18,
spaceAfter=30,
alignment=1 # 1表示居中
)
title = Paragraph("联系人信息", title_style)
elements.append(title)
# 获取筛选参数
branches_param = request.GET.get('branches')
category = request.GET.get('category')
contact_name = request.GET.get('contact_name')
# 构建查询条件
filters = Q()
# 分支机构筛选(支持多选)
if branches_param:
branch_ids = [bid.strip() for bid in branches_param.split(',') if bid.strip().isdigit()]
if branch_ids:
filters &= Q(branch_id__in=branch_ids)
# 联系人类别筛选
if category:
filters &= Q(category__contains=category)
# 联系人姓名筛选
if contact_name:
filters &= Q(name__icontains=contact_name)
# 获取筛选后的联系人
contacts = Contact.objects.filter(filters)
# 准备表格数据
data = [['分支机构', '分类', '姓名', '电话', '邮箱']] # 表头
for contact in contacts:
data.append([
contact.branch.name if contact.branch else '',
contact.category or '',
contact.name or '',
contact.phone or '',
contact.email or ''
])
# 创建表格
table = Table(data)
# 设置表格样式
style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), chinese_font), # 使用中文字体
('FONTSIZE', (0, 0), (-1, 0), 14),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
('FONTNAME', (0, 1), (-1, -1), chinese_font), # 使用中文字体
('GRID', (0, 0), (-1, -1), 1, colors.black)
])
table.setStyle(style)
# 添加表格到文档
elements.append(table)
# 构建PDF
doc.build(elements)
return response
def video_terminal_list(request): def video_terminal_list(request):
# 获取筛选参数 # 获取筛选参数
selected_branch = request.GET.get('branch', '') selected_branch = request.GET.get('branch', '')