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 ;
+ }
+
+ const iconProps = { style: { fontSize: 60, color: '#00b96b', marginBottom: 20 } };
+
+ switch(feature.icon_name) {
+ case 'SafetyCertificate':
+ return