This commit is contained in:
jeremygan2021
2026-02-02 14:07:47 +08:00
parent 5ada8c16e8
commit 6af90017d5
12 changed files with 388 additions and 46 deletions

271
.gitignore vendored
View File

@@ -1,19 +1,252 @@
default: # Django
- .DS_Store *.log
- .gitignore *.pot
- .git *.pyc
- .idea __pycache__/
- .vscode local_settings.py
- .cache db.sqlite3
- .dart_tool db.sqlite3-journal
- build media/
- coverage
- example/build # Django 迁移文件
- example/.dart_tool */migrations/__pycache__/
- example/.pub */migrations/*.pyc
- example/.flutter-plugins
- example/.flutter-plugins-dependencies # Django 静态文件
- example/ios/Flutter/flutter_export_environment.sh staticfiles/
- example/ios/Runner.xcworkspace static/
- "*.mtl"
- "*.obj" # 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

View File

@@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html from django.utils.html import format_html
from django.db.models import Sum 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 import qrcode
from io import BytesIO from io import BytesIO
import base64 import base64
@@ -11,6 +11,11 @@ admin.site.site_header = "量迹AI硬件销售管理后台"
admin.site.site_title = "量迹AI后台" admin.site.site_title = "量迹AI后台"
admin.site.index_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) @admin.register(WeChatPayConfig)
class WeChatPayConfigAdmin(admin.ModelAdmin): class WeChatPayConfigAdmin(admin.ModelAdmin):
list_display = ('app_id', 'mch_id', 'is_active', 'notify_url') 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_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone')
list_filter = ('chip_type', 'has_camera') list_filter = ('chip_type', 'has_camera')
search_fields = ('name', 'description') search_fields = ('name', 'description')
inlines = [ProductFeatureInline]
fieldsets = ( fieldsets = (
('基本信息', { ('基本信息', {
'fields': ('name', 'price', 'description') 'fields': ('name', 'price', 'description')

View File

@@ -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'],
},
),
]

View File

@@ -28,6 +28,27 @@ class ESP32Config(models.Model):
verbose_name_plural = "硬件配置 (小智参数)" 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): class Salesperson(models.Model):
""" """
销售人员模型 销售人员模型

View File

@@ -1,5 +1,22 @@
from rest_framework import serializers 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): class ServiceSerializer(serializers.ModelSerializer):
""" """
@@ -54,6 +71,7 @@ class ESP32ConfigSerializer(serializers.ModelSerializer):
ESP32配置序列化器 ESP32配置序列化器
""" """
display_detail_image = serializers.SerializerMethodField() display_detail_image = serializers.SerializerMethodField()
features = ProductFeatureSerializer(many=True, read_only=True)
class Meta: class Meta:
model = ESP32Config model = ESP32Config

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { Button, Row, Col, Tag, Statistic, Modal, Form, Input, InputNumber, message, Spin, Descriptions } from 'antd'; 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 { getConfigs, createOrder } from '../api';
import ModelViewer from '../components/ModelViewer'; import ModelViewer from '../components/ModelViewer';
import './ProductDetail.css'; import './ProductDetail.css';
@@ -78,6 +78,25 @@ const ProductDetail = () => {
const modelPaths = getModelPaths(product); const modelPaths = getModelPaths(product);
const renderIcon = (feature) => {
if (feature.display_icon) {
return <img src={feature.display_icon} alt={feature.title} style={{ width: 60, height: 60, objectFit: 'contain', marginBottom: 20 }} />;
}
const iconProps = { style: { fontSize: 60, color: '#00b96b', marginBottom: 20 } };
switch(feature.icon_name) {
case 'SafetyCertificate':
return <SafetyCertificateOutlined {...iconProps} />;
case 'Eye':
return <EyeOutlined {...iconProps} style={{ ...iconProps.style, color: '#1890ff' }} />;
case 'Thunderbolt':
return <ThunderboltOutlined {...iconProps} style={{ ...iconProps.style, color: '#faad14' }} />;
default:
return <StarOutlined {...iconProps} />;
}
};
if (loading) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" /></div>; if (loading) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" /></div>;
if (!product) return null; if (!product) return null;
@@ -114,7 +133,7 @@ const ProductDetail = () => {
</div> </div>
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 20, marginBottom: 40 }}> <div style={{ display: 'flex', alignItems: 'flex-end', gap: 20, marginBottom: 40 }}>
<Statistic title="售价" value={product.price} prefix="¥" styles={{ content: { color: '#00b96b', fontSize: 36 } }} titleStyle={{ color: '#888' }} /> <Statistic title="售价" value={product.price} prefix="¥" valueStyle={{ color: '#00b96b', fontSize: 36 }} titleStyle={{ color: '#888' }} />
</div> </div>
<Button type="primary" size="large" icon={<ShoppingCartOutlined />} onClick={() => setIsModalOpen(true)} style={{ height: 50, padding: '0 40px', fontSize: 18 }}> <Button type="primary" size="large" icon={<ShoppingCartOutlined />} onClick={() => setIsModalOpen(true)} style={{ height: 50, padding: '0 40px', fontSize: 18 }}>
@@ -123,8 +142,19 @@ const ProductDetail = () => {
</Col> </Col>
</Row> </Row>
{/* Long Image Introduction Simulation */} {/* Feature Section */}
<div style={{ marginTop: 100 }}> <div style={{ marginTop: 100 }}>
{product.features && product.features.length > 0 ? (
product.features.map((feature, index) => (
<div className="feature-section" key={index}>
{renderIcon(feature)}
<div className="feature-title">{feature.title}</div>
<div className="feature-desc">{feature.description}</div>
</div>
))
) : (
// Fallback content if no features are configured
<>
<div className="feature-section"> <div className="feature-section">
<SafetyCertificateOutlined style={{ fontSize: 60, color: '#00b96b', marginBottom: 20 }} /> <SafetyCertificateOutlined style={{ fontSize: 60, color: '#00b96b', marginBottom: 20 }} />
<div className="feature-title">工业级安全标准</div> <div className="feature-title">工业级安全标准</div>
@@ -148,6 +178,8 @@ const ProductDetail = () => {
{product.chip_type} 强劲核心提供高达 XX TOPS 的算力支持低功耗设计满足 24 小时全天候运行需求 {product.chip_type} 强劲核心提供高达 XX TOPS 的算力支持低功耗设计满足 24 小时全天候运行需求
</div> </div>
</div> </div>
</>
)}
{product.display_detail_image ? ( {product.display_detail_image ? (
<div style={{ margin: '60px 0', width: '100%', overflow: 'hidden', borderRadius: 12 }}> <div style={{ margin: '60px 0', width: '100%', overflow: 'hidden', borderRadius: 12 }}>