完善了通用excel上传解析和保存
This commit is contained in:
parent
acd5196715
commit
5e08ce8c1f
|
@ -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>
|
||||
|
|
|
@ -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"),
|
||||
]
|
|
@ -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
|
|
@ -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请求'})
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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 }}',
|
||||
|
|
Loading…
Reference in New Issue