feat: fix add product error

This commit is contained in:
hunterzhang 2025-05-15 11:58:54 +08:00
parent a738ca3aa3
commit baf81883e0
5 changed files with 4752 additions and 96 deletions

View File

@ -32,18 +32,44 @@
<div class="card-body">
<div class="row g-3 mb-4 align-items-center">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text bg-light"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="searchInput" placeholder="搜索商品名称或条码...">
</div>
<form method="get" action="{% url 'product_list' %}" class="mb-0">
<div class="input-group">
<span class="input-group-text bg-light"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="searchInput" name="search"
placeholder="搜索商品名称或条码..." value="{{ search_query }}">
<input type="hidden" name="sort" value="{{ sort_by }}">
<input type="hidden" name="category" value="{{ selected_category }}">
<input type="hidden" name="status" value="{{ selected_status }}">
<button type="submit" class="btn btn-primary">搜索</button>
</div>
</form>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-md-end">
<div class="btn-group" id="categoryFilter">
<button type="button" class="btn btn-outline-primary active" data-category="">全部</button>
{% for category in categories %}
<button type="button" class="btn btn-outline-primary" data-category="{{ category.id }}">{{ category.name }}</button>
{% endfor %}
<div class="btn-group me-2">
<a href="{% url 'product_list' %}?sort=updated{% if search_query %}&search={{ search_query }}{% endif %}{% if selected_category %}&category={{ selected_category }}{% endif %}{% if selected_status %}&status={{ selected_status }}{% endif %}"
class="btn btn-outline-secondary {% if sort_by == 'updated' %}active{% endif %}">
<i class="bi bi-clock-history"></i> 最近更新
</a>
<a href="{% url 'product_list' %}?sort=name{% if search_query %}&search={{ search_query }}{% endif %}{% if selected_category %}&category={{ selected_category }}{% endif %}{% if selected_status %}&status={{ selected_status }}{% endif %}"
class="btn btn-outline-secondary {% if sort_by == 'name' %}active{% endif %}">
<i class="bi bi-sort-alpha-down"></i> 名称
</a>
<a href="{% url 'product_list' %}?sort=created{% if search_query %}&search={{ search_query }}{% endif %}{% if selected_category %}&category={{ selected_category }}{% endif %}{% if selected_status %}&status={{ selected_status }}{% endif %}"
class="btn btn-outline-secondary {% if sort_by == 'created' %}active{% endif %}">
<i class="bi bi-calendar-plus"></i> 创建时间
</a>
</div>
<div class="form-group ms-2" style="min-width: 150px;">
<div class="input-group">
<span class="input-group-text bg-light"><i class="bi bi-tag"></i></span>
<select id="categoryDropdown" class="form-select" aria-label="分类筛选">
<option value="" {% if not selected_category %}selected{% endif %}>全部分类</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if selected_category == category.id|stringformat:'s' %}selected{% endif %}>{{ category.name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
@ -128,6 +154,56 @@
</tbody>
</table>
</div>
<!-- 添加分页控件 -->
{% if page_obj.has_other_pages %}
<div class="d-flex justify-content-center mt-4">
<nav aria-label="商品列表分页">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if selected_category %}&category={{ selected_category }}{% endif %}{% if sort_by %}&sort={{ sort_by }}{% endif %}{% if selected_status %}&status={{ selected_status }}{% endif %}" aria-label="上一页">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="上一页">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for i in page_obj.paginator.page_range %}
{% if page_obj.number == i %}
<li class="page-item active">
<a class="page-link" href="#">{{ i }}</a>
</li>
{% elif i > page_obj.number|add:"-4" and i < page_obj.number|add:"4" %}
<li class="page-item">
<a class="page-link" href="?page={{ i }}{% if search_query %}&search={{ search_query }}{% endif %}{% if selected_category %}&category={{ selected_category }}{% endif %}{% if sort_by %}&sort={{ sort_by }}{% endif %}{% if selected_status %}&status={{ selected_status }}{% endif %}">{{ i }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if selected_category %}&category={{ selected_category }}{% endif %}{% if sort_by %}&sort={{ sort_by }}{% endif %}{% if selected_status %}&status={{ selected_status }}{% endif %}" aria-label="下一页">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="下一页">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
</div>
</div>
</div>
@ -135,49 +211,46 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
// 搜索功能
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', filterProducts);
// 删除前端搜索过滤逻辑,由后端处理搜索和筛选
// 分类筛选功能
const categoryButtons = document.querySelectorAll('#categoryFilter button');
categoryButtons.forEach(button => {
button.addEventListener('click', function() {
// 移除所有按钮的active类
categoryButtons.forEach(btn => btn.classList.remove('active'));
// 给当前点击的按钮添加active类
this.classList.add('active');
filterProducts();
// 添加分页导航
document.querySelectorAll('.page-link').forEach(link => {
link.addEventListener('click', function(e) {
if (this.getAttribute('href') === '#') {
e.preventDefault();
}
});
});
function filterProducts() {
const searchTerm = searchInput.value.toLowerCase();
const activeCategory = document.querySelector('#categoryFilter button.active').dataset.category;
const rows = document.querySelectorAll('tbody tr');
rows.forEach(row => {
// 忽略空行
if (!row.querySelector('td:first-child')) return;
// 分类下拉列表变化处理
const categoryDropdown = document.getElementById('categoryDropdown');
if (categoryDropdown) {
categoryDropdown.addEventListener('change', function() {
// 构建URL
let url = '{% url 'product_list' %}?';
const productName = row.querySelector('h6')?.textContent.toLowerCase() || '';
const barcode = row.querySelector('small')?.textContent.toLowerCase() || '';
const rowCategory = row.dataset.category;
// 检查搜索条件
const matchesSearch = productName.includes(searchTerm) || barcode.includes(searchTerm);
// 检查分类条件
const matchesCategory = !activeCategory || rowCategory === activeCategory;
// 同时满足搜索和分类条件才显示
if (matchesSearch && matchesCategory) {
row.style.display = '';
} else {
row.style.display = 'none';
// 添加分类参数 (如果有选择分类)
if (this.value) {
url += 'category=' + this.value;
}
// 添加排序参数 (如果存在)
{% if sort_by %}
url += (url.endsWith('?') ? '' : '&') + 'sort={{ sort_by }}';
{% endif %}
// 添加搜索参数 (如果存在)
{% if search_query %}
url += (url.endsWith('?') ? '' : '&') + 'search={{ search_query }}';
{% endif %}
// 添加状态参数 (如果存在)
{% if selected_status %}
url += (url.endsWith('?') ? '' : '&') + 'status={{ selected_status }}';
{% endif %}
// 导航到构建的URL
window.location.href = url;
});
}
});

View File

@ -90,10 +90,13 @@ def product_list(request):
search_query = request.GET.get('search', '')
category_id = request.GET.get('category', '')
status = request.GET.get('status', 'active') # 默认显示活跃商品
sort_by = request.GET.get('sort', 'name')
sort_by = request.GET.get('sort', 'updated') # 修改默认排序为更新时间
print(f"DEBUG: 列表筛选参数 - 搜索: {search_query}, 分类: {category_id}, 状态: {status}, 排序: {sort_by}")
# 基本查询集
products = Product.objects.select_related('category').all()
print(f"DEBUG: 初始查询集数量: {products.count()}")
# 应用筛选
if search_query:
@ -109,6 +112,7 @@ def product_list(request):
# 状态筛选
if status == 'active':
products = products.filter(is_active=True)
print(f"DEBUG: 应用活跃状态筛选后的数量: {products.count()}")
elif status == 'inactive':
products = products.filter(is_active=False)
@ -121,6 +125,10 @@ def product_list(request):
products = products.order_by('category__name', 'name')
elif sort_by == 'created':
products = products.order_by('-created_at')
elif sort_by == 'updated': # 添加按更新时间排序
products = products.order_by('-updated_at')
else: # 默认按更新时间降序
products = products.order_by('-updated_at')
# 分页
paginator = Paginator(products, 15) # 每页15个商品
@ -134,6 +142,8 @@ def product_list(request):
total_products = Product.objects.count()
active_products = Product.objects.filter(is_active=True).count()
print(f"DEBUG: 总商品数: {total_products}, 活跃商品数: {active_products}, 当前页面商品数: {len(page_obj)}")
context = {
'page_obj': page_obj,
'categories': categories,
@ -188,40 +198,47 @@ def product_create(request):
form = ProductForm(request.POST)
image_formset = ProductImageFormSet(request.POST, request.FILES, prefix='images')
if form.is_valid() and image_formset.is_valid():
# 修改验证逻辑,只检查表单是否有效,不强制检查图片表单集
if form.is_valid():
# 保存商品数据
product = form.save(commit=False)
product.created_by = request.user
product.is_active = True # 确保商品默认为活跃状态
product.save()
# 保存商品图片
for image_form in image_formset:
if image_form.cleaned_data and not image_form.cleaned_data.get('DELETE'):
image = image_form.save(commit=False)
image.product = product
# 处理图片文件
if image.image:
# 生成缩略图
thumbnail = generate_thumbnail(image.image, (300, 300))
# 只有当图片表单集有效时才处理图片
if image_formset.is_valid():
# 保存商品图片
for image_form in image_formset:
if image_form.cleaned_data and not image_form.cleaned_data.get('DELETE'):
image = image_form.save(commit=False)
image.product = product
# 保存缩略图
thumb_name = f'thumb_{uuid.uuid4()}.jpg'
thumb_path = f'products/thumbnails/{thumb_name}'
thumb_file = io.BytesIO()
thumbnail.save(thumb_file, format='JPEG')
# 处理图片文件
if image.image:
# 生成缩略图
thumbnail = generate_thumbnail(image.image, (300, 300))
# 保存缩略图
thumb_name = f'thumb_{uuid.uuid4()}.jpg'
thumb_path = f'products/thumbnails/{thumb_name}'
thumb_file = io.BytesIO()
thumbnail.save(thumb_file, format='JPEG')
# 设置缩略图路径
image.thumbnail = thumb_path
# 设置缩略图路径
image.thumbnail = thumb_path
image.save()
image.save()
# 创建初始库存记录
warning_level = 10 # 设置一个默认的预警值
if 'warning_level' in form.cleaned_data and form.cleaned_data['warning_level'] is not None:
warning_level = form.cleaned_data['warning_level']
Inventory.objects.create(
product=product,
quantity=0,
warning_level=form.cleaned_data.get('warning_level', 5)
warning_level=warning_level
)
messages.success(request, f'商品 {product.name} 创建成功')
@ -230,7 +247,8 @@ def product_create(request):
if 'next' in request.POST and request.POST['next'] == 'bulk':
return redirect('product_bulk_create')
return redirect('product_detail', pk=product.id)
# 修改重定向,解决模板不存在的问题
return redirect('product_list')
else:
form = ProductForm()
image_formset = ProductImageFormSet(prefix='images')
@ -263,53 +281,61 @@ def product_update(request, pk):
form = ProductForm(request.POST, instance=product)
image_formset = ProductImageFormSet(request.POST, request.FILES, prefix='images', instance=product)
if form.is_valid() and image_formset.is_valid():
# 修改验证逻辑,只检查表单是否有效,不强制检查图片表单集
if form.is_valid():
# 保存商品数据
product = form.save(commit=False)
product.updated_at = timezone.now()
product.updated_by = request.user
product.save()
# 保存商品图片
for image_form in image_formset:
if image_form.cleaned_data:
if image_form.cleaned_data.get('DELETE'):
if image_form.instance.pk:
image_form.instance.delete()
else:
image = image_form.save(commit=False)
image.product = product
# 处理图片文件
if image.image and not image.thumbnail:
# 生成缩略图
thumbnail = generate_thumbnail(image.image, (300, 300))
# 只有当图片表单集有效时才处理图片
if image_formset.is_valid():
# 保存商品图片
for image_form in image_formset:
if image_form.cleaned_data:
if image_form.cleaned_data.get('DELETE'):
if image_form.instance.pk:
image_form.instance.delete()
else:
image = image_form.save(commit=False)
image.product = product
# 保存缩略图
thumb_name = f'thumb_{uuid.uuid4()}.jpg'
thumb_path = f'products/thumbnails/{thumb_name}'
thumb_file = io.BytesIO()
thumbnail.save(thumb_file, format='JPEG')
# 处理图片文件
if image.image and not image.thumbnail:
# 生成缩略图
thumbnail = generate_thumbnail(image.image, (300, 300))
# 保存缩略图
thumb_name = f'thumb_{uuid.uuid4()}.jpg'
thumb_path = f'products/thumbnails/{thumb_name}'
thumb_file = io.BytesIO()
thumbnail.save(thumb_file, format='JPEG')
# 设置缩略图路径
image.thumbnail = thumb_path
# 设置缩略图路径
image.thumbnail = thumb_path
image.save()
image.save()
# 更新库存预警级别
warning_level = 10 # 设置一个默认的预警值
if 'warning_level' in form.cleaned_data and form.cleaned_data['warning_level'] is not None:
warning_level = form.cleaned_data['warning_level']
try:
inventory = Inventory.objects.get(product=product)
inventory.warning_level = form.cleaned_data.get('warning_level', inventory.warning_level)
inventory.warning_level = warning_level
inventory.save()
except Inventory.DoesNotExist:
Inventory.objects.create(
product=product,
quantity=0,
warning_level=form.cleaned_data.get('warning_level', 5)
warning_level=warning_level
)
messages.success(request, f'商品 {product.name} 更新成功')
return redirect('product_detail', pk=product.id)
# 修改重定向,解决模板不存在的问题
return redirect('product_list')
else:
form = ProductForm(instance=product)
# 设置库存预警级别

1511
product_form.html Normal file

File diff suppressed because it is too large Load Diff

1519
product_list.html Normal file

File diff suppressed because it is too large Load Diff

1527
product_response.html Normal file

File diff suppressed because it is too large Load Diff