diff --git a/.gitignore b/.gitignore index c4fc064..4c1ac06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,252 @@ -default: - - .DS_Store - - .gitignore - - .git - - .idea - - .vscode - - .cache - - .dart_tool - - build - - coverage - - example/build - - example/.dart_tool - - example/.pub - - example/.flutter-plugins - - example/.flutter-plugins-dependencies - - example/ios/Flutter/flutter_export_environment.sh - - example/ios/Runner.xcworkspace - - "*.mtl" - - "*.obj" +# Django +*.log +*.pot +*.pyc +__pycache__/ +local_settings.py +db.sqlite3 +db.sqlite3-journal +media/ + +# Django 迁移文件 +*/migrations/__pycache__/ +*/migrations/*.pyc + +# Django 静态文件 +staticfiles/ +static/ + +# Python +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Python 虚拟环境 +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ + +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json +yarn.lock + +# 前端构建文件 +dist/ +build/ +*.map + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Windows +Thumbs.db +ehthumbs.db +*.stackdump + +# 大文件和媒体文件 +*.mp4 +*.mp3 +*.avi +*.mov +*.wmv +*.flv +*.mkv +*.wav +*.flac +*.aac +*.wma +*.m4a +*.m4v +*.3gp +*.3g2 +*.asf +*.rm +*.rmvb +*.vob +*.mpg +*.mpeg +*.m2v +*.m4v +*.svi +*.3gpp +*.3gpp2 + +# 图片文件(保留必要的,忽略大图片) +*.psd +*.ai +*.eps +*.raw +*.cr2 +*.nef +*.orf +*.sr2 +*.tiff +*.tif +*.bmp +*.ico +# 保留 PNG、JPG、JPEG、SVG 用于网站显示 +# *.png +# *.jpg +# *.jpeg +# *.svg + +# 3D模型文件(大文件) +*.obj +*.mtl +*.fbx +*.dae +*.3ds +*.max +*.ma +*.mb +*.blend +*.c4d +*.lwo +*.lws +*.skp +*.x3d +*.x3db +*.x3dv +*.wrl +*.wrz +*.ply +*.stl +*.stp +*.step +*.igs +*.iges + +# 压缩文件 +*.zip +*.rar +*.7z +*.tar +*.gz +*.bz2 +*.xz +*.tar.gz +*.tar.bz2 +*.tar.xz +*.tgz +*.tbz2 +*.txz + +# 文档文件(大文件) +*.pdf +*.doc +*.docx +*.xls +*.xlsx +*.ppt +*.pptx +*.odt +*.ods +*.odp + +# 数据库文件 +*.sql +*.sqlite +*.sqlite3 +*.db +*.mdb +*.accdb + +# 备份文件 +*.bak +*.backup +*.old +*.orig +*.tmp +*.temp +*.swp +*.swo + +# 日志文件 +*.log +*.log.* +logs/ + +# 缓存文件 +.cache/ +*.cache +*.tmp + +# 配置文件(敏感信息) +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Docker +.dockerignore + +# Git +.git/ + +# 其他大文件 +*.iso +*.dmg +*.img +*.vmdk +*.vdi +*.vhd +*.vhdx +*.qcow +*.qcow2 +*.ova +*.ovf + +# 前端特定忽略 +frontend/dist/ +frontend/build/ +frontend/node_modules/ + +# 后端特定忽略 +backend/db.sqlite3 +backend/__pycache__/ +backend/*.pyc +backend/media/ +backend/static/ +backend/venv/ +backend/env/ + +# 项目特定的大文件路径 +frontend/public/3d*/ +frontend/public/*.obj +frontend/public/*.mtl +frontend/dist/3d*/ +frontend/dist/*.obj +frontend/dist/*.mtl \ No newline at end of file diff --git a/backend/shop/__pycache__/admin.cpython-312.pyc b/backend/shop/__pycache__/admin.cpython-312.pyc index 5f826e1..47f0e05 100644 Binary files a/backend/shop/__pycache__/admin.cpython-312.pyc and b/backend/shop/__pycache__/admin.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/admin.cpython-313.pyc b/backend/shop/__pycache__/admin.cpython-313.pyc index d1bc2ad..3935e94 100644 Binary files a/backend/shop/__pycache__/admin.cpython-313.pyc and b/backend/shop/__pycache__/admin.cpython-313.pyc differ diff --git a/backend/shop/__pycache__/models.cpython-312.pyc b/backend/shop/__pycache__/models.cpython-312.pyc index fb02aa9..6b7e3df 100644 Binary files a/backend/shop/__pycache__/models.cpython-312.pyc and b/backend/shop/__pycache__/models.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/models.cpython-313.pyc b/backend/shop/__pycache__/models.cpython-313.pyc index da80bf3..6d22e88 100644 Binary files a/backend/shop/__pycache__/models.cpython-313.pyc and b/backend/shop/__pycache__/models.cpython-313.pyc differ diff --git a/backend/shop/__pycache__/serializers.cpython-312.pyc b/backend/shop/__pycache__/serializers.cpython-312.pyc index adb8b09..8de83ef 100644 Binary files a/backend/shop/__pycache__/serializers.cpython-312.pyc and b/backend/shop/__pycache__/serializers.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/serializers.cpython-313.pyc b/backend/shop/__pycache__/serializers.cpython-313.pyc index 445d1ee..124ad0c 100644 Binary files a/backend/shop/__pycache__/serializers.cpython-313.pyc and b/backend/shop/__pycache__/serializers.cpython-313.pyc differ diff --git a/backend/shop/admin.py b/backend/shop/admin.py index 8dadd4e..50e70b7 100644 --- a/backend/shop/admin.py +++ b/backend/shop/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.utils.html import format_html from django.db.models import Sum -from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService +from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService, ProductFeature import qrcode from io import BytesIO import base64 @@ -11,6 +11,11 @@ admin.site.site_header = "量迹AI硬件销售管理后台" admin.site.site_title = "量迹AI后台" admin.site.index_title = "欢迎使用量迹AI管理系统" +class ProductFeatureInline(admin.TabularInline): + model = ProductFeature + extra = 1 + fields = ('title', 'description', 'icon_name', 'icon_image', 'icon_url', 'order') + @admin.register(WeChatPayConfig) class WeChatPayConfigAdmin(admin.ModelAdmin): list_display = ('app_id', 'mch_id', 'is_active', 'notify_url') @@ -33,6 +38,7 @@ class ESP32ConfigAdmin(admin.ModelAdmin): list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone') list_filter = ('chip_type', 'has_camera') search_fields = ('name', 'description') + inlines = [ProductFeatureInline] fieldsets = ( ('基本信息', { 'fields': ('name', 'price', 'description') diff --git a/backend/shop/migrations/0007_productfeature.py b/backend/shop/migrations/0007_productfeature.py new file mode 100644 index 0000000..a8e0c21 --- /dev/null +++ b/backend/shop/migrations/0007_productfeature.py @@ -0,0 +1,32 @@ +# Generated by Django 6.0.1 on 2026-02-02 06:04 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0006_arservice_esp32config_detail_image_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ProductFeature', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=50, verbose_name='特性标题')), + ('description', models.TextField(verbose_name='特性描述')), + ('icon_name', models.CharField(blank=True, help_text='例如: SafetyCertificate, Eye, Thunderbolt', max_length=50, null=True, verbose_name='Antd图标名称')), + ('icon_image', models.ImageField(blank=True, null=True, upload_to='products/features/', verbose_name='特性图标 (上传)')), + ('icon_url', models.URLField(blank=True, null=True, verbose_name='特性图标 (URL)')), + ('order', models.IntegerField(default=0, help_text='数字越小越靠前', verbose_name='排序权重')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='shop.esp32config', verbose_name='所属产品')), + ], + options={ + 'verbose_name': '产品特性', + 'verbose_name_plural': '产品特性', + 'ordering': ['order'], + }, + ), + ] diff --git a/backend/shop/models.py b/backend/shop/models.py index 9a9ee37..017467f 100644 --- a/backend/shop/models.py +++ b/backend/shop/models.py @@ -28,6 +28,27 @@ class ESP32Config(models.Model): verbose_name_plural = "硬件配置 (小智参数)" +class ProductFeature(models.Model): + """ + 产品特性模型 (关联到具体硬件配置) + """ + product = models.ForeignKey(ESP32Config, on_delete=models.CASCADE, related_name='features', verbose_name="所属产品") + title = models.CharField(max_length=50, verbose_name="特性标题") + description = models.TextField(verbose_name="特性描述") + icon_name = models.CharField(max_length=50, blank=True, null=True, verbose_name="Antd图标名称", help_text="例如: SafetyCertificate, Eye, Thunderbolt") + icon_image = models.ImageField(upload_to='products/features/', blank=True, null=True, verbose_name="特性图标 (上传)") + icon_url = models.URLField(blank=True, null=True, verbose_name="特性图标 (URL)") + order = models.IntegerField(default=0, verbose_name="排序权重", help_text="数字越小越靠前") + + def __str__(self): + return f"{self.product.name} - {self.title}" + + class Meta: + verbose_name = "产品特性" + verbose_name_plural = "产品特性" + ordering = ['order'] + + class Salesperson(models.Model): """ 销售人员模型 diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py index 5436361..ce5ae6d 100644 --- a/backend/shop/serializers.py +++ b/backend/shop/serializers.py @@ -1,5 +1,22 @@ from rest_framework import serializers -from .models import ESP32Config, Order, Salesperson, Service, ARService +from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature + +class ProductFeatureSerializer(serializers.ModelSerializer): + """ + 产品特性序列化器 + """ + display_icon = serializers.SerializerMethodField() + + class Meta: + model = ProductFeature + fields = ['title', 'description', 'icon_name', 'display_icon', 'order'] + + def get_display_icon(self, obj): + if obj.icon_url: + return obj.icon_url + if obj.icon_image: + return obj.icon_image.url + return None class ServiceSerializer(serializers.ModelSerializer): """ @@ -54,6 +71,7 @@ class ESP32ConfigSerializer(serializers.ModelSerializer): ESP32配置序列化器 """ display_detail_image = serializers.SerializerMethodField() + features = ProductFeatureSerializer(many=True, read_only=True) class Meta: model = ESP32Config diff --git a/frontend/src/pages/ProductDetail.jsx b/frontend/src/pages/ProductDetail.jsx index c9f9721..078928c 100644 --- a/frontend/src/pages/ProductDetail.jsx +++ b/frontend/src/pages/ProductDetail.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { Button, Row, Col, Tag, Statistic, Modal, Form, Input, InputNumber, message, Spin, Descriptions } from 'antd'; -import { ShoppingCartOutlined, SafetyCertificateOutlined, ThunderboltOutlined, EyeOutlined } from '@ant-design/icons'; +import { ShoppingCartOutlined, SafetyCertificateOutlined, ThunderboltOutlined, EyeOutlined, StarOutlined } from '@ant-design/icons'; import { getConfigs, createOrder } from '../api'; import ModelViewer from '../components/ModelViewer'; import './ProductDetail.css'; @@ -78,6 +78,25 @@ const ProductDetail = () => { const modelPaths = getModelPaths(product); + const renderIcon = (feature) => { + if (feature.display_icon) { + return {feature.title}; + } + + const iconProps = { style: { fontSize: 60, color: '#00b96b', marginBottom: 20 } }; + + switch(feature.icon_name) { + case 'SafetyCertificate': + return ; + case 'Eye': + return ; + case 'Thunderbolt': + return ; + default: + return ; + } + }; + if (loading) return
; if (!product) return null; @@ -114,7 +133,7 @@ const ProductDetail = () => {
- +