完善了通用excel上传解析和保存

This commit is contained in:
王思川 2024-06-17 23:40:16 +08:00
parent acd5196715
commit 5e08ce8c1f
6 changed files with 169 additions and 8 deletions

View File

@ -40,16 +40,21 @@
th {
background-color: #f2f2f2;
}
.error-cell {
background-color: #f8d7da; /* 红色背景 */
border-color: #f5c2c7; /* 红色边框 */
}
</style>
</head>
<body>
<div id="alertContainer" style="position: fixed; top: 10px; right: 10px; z-index: 9999; width: 300px;"></div>
<div class="page-wrapper">
<form id="preview-form" method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<div class="modal-header">
<h3 class="modal-title" id="excelPreviewModalTitle">上传文件预览</h3>
<a href="javascript:history.back()" class="btn btn-primary btn-lg" data-bs-toggle="tooltip" data-bs-original-title="返回">返回列表</a>
<a href="javascript:void(0);" class="btn btn-primary" data-bs-toggle="tooltip" data-bs-original-title="返回" onclick="history.back(); ">返回</a>
</div>
<div class="modal-body">
<div class="table-container">
@ -80,9 +85,6 @@
<div class="modal-footer">
<div id="pagination">
<span id="totalData" class="mx-2">数据总数: {{ table_data|length }}</span>
<span id="pageInfo" class="mx-2">第 1 页, 共 1 页</span>
<button id="prevPage" class="btn btn-secondary" type="button">上一页</button>
<button id="nextPage" class="btn btn-secondary" type="button">下一页</button>
</div>
<div id="actions">
<button id="saveButton" type="button" class="btn btn-primary">保存</button>
@ -93,6 +95,60 @@
<script src="{% static 'js/vendor-all.min.js' %}"></script>
<script src="{% static 'plugins/bootstrap/js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/custom.js' %}"></script>
<script>
document.getElementById('saveButton').addEventListener('click', function() {
const table = document.getElementById('form-input-table');
const headers = Array.from(table.rows[0].cells).map(cell => cell.textContent);
const data = [];
// 清除之前的错误标记
const inputs = table.querySelectorAll('input');
inputs.forEach(input => {
input.classList.remove('error-cell');
});
for (let i = 1; i < table.rows.length; i++) {
const row = table.rows[i];
const rowData = {};
for (let j = 1; j < row.cells.length; j++) { // 跳过序号列
rowData[headers[j]] = row.cells[j].querySelector('input').value;
}
data.push(rowData);
}
fetch("{% url 'ep_common_save' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({ data: data })
})
.then(response => response.json())
.then(result => {
if (result.status === 'success') {
showAlert('success', result.message);
} else {
const errorMessage = result.message + '<br>' + result.errors.join('<br>');
showAlert('danger', errorMessage);
result.errors.forEach(error => {
const regex = /第【(\d+)】行【(.+?)】字段输入错误/;
const matches = error.match(regex);
if (matches) {
const row = parseInt(matches[1], 10);
const field = matches[2];
const column = Array.from(table.rows[0].cells).findIndex(cell => cell.textContent.trim() === field.trim());
if (column !== -1 && table.rows[row] && table.rows[row].cells[column]) {
table.rows[row].cells[column].querySelector('input').classList.add('error-cell');
}
}
});
}
});
});
</script>
</body>
</html>

View File

@ -4,5 +4,6 @@ from excel_parser.views import *
urlpatterns = [
path('excel_preview/', excel_preview, name='excel_preview'),
path('dl_excel_tpl/<str:template_name>/', dl_excel_tpl, name='dl_excel_tpl'),
path('common_parse/', common_parse, name="ep_common_parse")
path('common_parse/', common_parse, name="ep_common_parse"),
path('common_save/', common_save, name="ep_common_save"),
]

60
excel_parser/utils.py Normal file
View File

@ -0,0 +1,60 @@
from django.core.exceptions import ValidationError
from django.apps import apps
class ExcelDataSaver:
def __init__(self, model_name, data):
app_name, model_name = model_name.split('.')
self.model = apps.get_model(app_name, model_name)
self.data = data
self.field_mapping, self.reverse_field_mapping = self._generate_field_mapping()
self.errors = []
def _generate_field_mapping(self):
field_mapping = {}
reverse_field_mapping = {}
for field in self.model._meta.get_fields():
if hasattr(field, 'verbose_name'):
field_mapping[field.verbose_name] = field.name
reverse_field_mapping[field.name] = field.verbose_name
return field_mapping, reverse_field_mapping
def validate_data(self):
for row_num, row in enumerate(self.data):
instance = self.model()
for field, value in row.items():
actual_field = self.field_mapping.get(field)
if not actual_field:
self.errors.append(f"第【{row_num + 1}】行找不到对应的模型字段: 【{self.reverse_field_mapping[field] + ':' + field}")
continue
setattr(instance, actual_field, value)
# 在设置所有字段值之后再调用 full_clean
try:
instance.full_clean()
except ValidationError as e:
for field, errors in e.message_dict.items():
for error in errors:
self.errors.append(f"第【{row_num + 1}】行【{self.reverse_field_mapping[field]}】字段输入错误: {error}")
return len(self.errors) == 0
def save_data(self):
if self.validate_data():
for row in self.data:
row_data = {self.field_mapping[field]: value for field, value in row.items() if field in self.field_mapping}
instance = self.model(**row_data)
instance.save()
return True
return False
def get_errors(self):
return self.errors
def add_custom_validation(self, custom_validation_func):
self.custom_validation_func = custom_validation_func
self.validate_data = self._validate_data_with_custom
def _validate_data_with_custom(self):
basic_valid = self.validate_data()
custom_valid = self.custom_validation_func(self.data)
return basic_valid and custom_valid

View File

@ -1,3 +1,4 @@
import json
import urllib.parse
from datetime import datetime
@ -8,6 +9,8 @@ from django.shortcuts import render
from django.urls import reverse
from openpyxl.reader.excel import load_workbook
from excel_parser.utils import ExcelDataSaver
@login_required
def dl_excel_tpl(request, template_name):
@ -62,6 +65,7 @@ def common_parse(request):
# 获取上传的 Excel 文件和模板名称
excel_file = request.FILES['file']
template_name = request.POST.get('template_name')
excel_valid_model = request.POST.get('excel_valid_model')
# 加载上传的 Excel 文件
wb = load_workbook(excel_file)
@ -100,6 +104,7 @@ def common_parse(request):
# 存储解析数据到 session 中
request.session['columns'] = uploaded_columns
request.session['table_data'] = data
request.session['excel_valid_model'] = excel_valid_model
# 返回新的页面 URL
return JsonResponse({'redirect_url': reverse("excel_preview")})
@ -122,10 +127,39 @@ def excel_preview(request):
返回:
HttpResponse: 渲染 'excel_preview_table.html' 模板并包含会话中的列名和表格数据
"""
# 从会话中获取列名列表
# 从会话中获取数据
columns = request.session.get('columns', [])
# 从会话中获取表格数据列表
table_data = request.session.get('table_data', [])
excel_valid_model = request.session.get('excel_valid_model', '')
context = {
'columns': columns,
'table_data': table_data,
'excel_valid_model': excel_valid_model
}
# 渲染 HTML 模板 'excel_preview_table.html',并传递列名和表格数据
return render(request, 'excel_preview_table.html', {'columns': columns, 'table_data': table_data})
return render(request, 'excel_preview_table.html', context)
def common_save(request):
if request.method == 'POST':
try:
body = json.loads(request.body)
data = body.get('data', None)
model_name = request.session.get('excel_valid_model', None)
if not data or not model_name:
return JsonResponse({'status': 'error', 'message': '无效的数据或模型'})
data_saver = ExcelDataSaver(model_name, data)
if data_saver.save_data():
return JsonResponse({'status': 'success', 'message': '数据已成功保存'})
else:
return JsonResponse({'status': 'error', 'message': '数据验证失败', 'errors': data_saver.get_errors()})
except json.JSONDecodeError:
return JsonResponse({'status': 'error', 'message': '无法解析请求的JSON数据'})
return JsonResponse({'status': 'error', 'message': '仅支持POST请求'})

8
static/js/custom.js Normal file
View File

@ -0,0 +1,8 @@
function showAlert(type, message) {
const alertHtml = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
<strong>操作提示</strong> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
</div>`;
$('#alertContainer').html(alertHtml);
}

View File

@ -204,6 +204,7 @@
const deleteUrl = "{{ delete_url }}";
let targetIdToDelete = null;
var templateName = "{{ excel_upload_config.template_name }}";
var modelConfig = "{{ model_config }}";
$(document).ready(function () {
function getCookie(name) {
@ -355,6 +356,7 @@
var formData = new FormData();
formData.append('file', inputFile); // 将文件添加到 FormData 中
formData.append('template_name', templateName);
formData.append('excel_valid_model', modelConfig);
$.ajax({
url: '{{ excel_upload_config.parse_url }}',