first commit
207
README.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# 量极AI硬件商城
|
||||
|
||||
一个基于React和Django的AI硬件在线商城系统,提供硬件配置展示、订单管理和支付功能。
|
||||
|
||||
## 🚀 项目概述
|
||||
|
||||
量极AI硬件商城是一个全栈Web应用程序,专注于AI硬件产品的在线销售。系统采用前后端分离架构,前端使用React + Vite + Ant Design,后端使用Django REST Framework。
|
||||
|
||||
## 📋 功能特性
|
||||
|
||||
### 前端功能
|
||||
- 🛍️ 硬件配置展示和选择
|
||||
- 🛒 购物车功能
|
||||
- 📋 订单创建和管理
|
||||
- 💳 支付流程集成
|
||||
- 🔗 推广码支持
|
||||
- 📱 响应式设计
|
||||
|
||||
### 后端功能
|
||||
- 🏪 产品配置管理
|
||||
- 📦 订单处理
|
||||
- 💰 支付接口
|
||||
- 👥 用户管理
|
||||
- 📊 数据统计
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
### 前端技术
|
||||
- **React 19** - 现代化UI库
|
||||
- **Vite** - 快速构建工具
|
||||
- **Ant Design** - 企业级UI组件库
|
||||
- **React Router** - 路由管理
|
||||
- **Axios** - HTTP客户端
|
||||
|
||||
### 后端技术
|
||||
- **Django 6.0** - Python Web框架
|
||||
- **Django REST Framework** - RESTful API
|
||||
- **PostgreSQL** - 数据库
|
||||
- **CORS Headers** - 跨域支持
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
Quant-Speed_ai_hardware/
|
||||
├── frontend/ # React前端应用
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React组件
|
||||
│ │ │ └── HardwareShop.jsx
|
||||
│ │ ├── App.jsx # 主应用组件
|
||||
│ │ ├── api.js # API接口封装
|
||||
│ │ └── main.jsx # 应用入口
|
||||
│ ├── package.json # 前端依赖配置
|
||||
│ └── vite.config.js # Vite配置
|
||||
├── backend/ # Django后端应用
|
||||
│ ├── config/ # Django配置
|
||||
│ │ ├── settings.py # 主配置文件
|
||||
│ │ ├── urls.py # URL路由配置
|
||||
│ │ └── wsgi.py # WSGI配置
|
||||
│ ├── shop/ # 商城应用
|
||||
│ │ ├── models.py # 数据模型
|
||||
│ │ ├── views.py # 视图函数
|
||||
│ │ ├── serializers.py # 序列化器
|
||||
│ │ └── urls.py # 应用路由
|
||||
│ ├── manage.py # Django管理脚本
|
||||
│ └── populate_db.py # 数据库初始化脚本
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
- Node.js 18+
|
||||
- Python 3.8+
|
||||
- PostgreSQL 12+
|
||||
|
||||
### 前端安装
|
||||
|
||||
```bash
|
||||
# 进入前端目录
|
||||
cd frontend
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 后端安装
|
||||
|
||||
```bash
|
||||
# 进入后端目录
|
||||
cd backend
|
||||
|
||||
# 创建虚拟环境
|
||||
python -m venv venv
|
||||
|
||||
# 激活虚拟环境
|
||||
# Windows
|
||||
venv\Scripts\activate
|
||||
# macOS/Linux
|
||||
source venv/bin/activate
|
||||
|
||||
# 安装依赖
|
||||
pip install django djangorestframework django-cors-headers psycopg2-binary
|
||||
|
||||
# 数据库配置
|
||||
# 编辑 config/settings.py 中的数据库配置
|
||||
|
||||
# 运行数据库迁移
|
||||
python manage.py migrate
|
||||
|
||||
# 创建超级用户
|
||||
python manage.py createsuperuser
|
||||
|
||||
# 启动开发服务器
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
### 数据库初始化
|
||||
|
||||
```bash
|
||||
# 运行数据库填充脚本
|
||||
python populate_db.py
|
||||
```
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 前端配置
|
||||
- **Vite配置**: `frontend/vite.config.js`
|
||||
- **环境变量**: 支持 `.env` 文件配置
|
||||
|
||||
### 后端配置
|
||||
- **Django设置**: `backend/config/settings.py`
|
||||
- **数据库**: PostgreSQL配置
|
||||
- **CORS**: 跨域请求配置
|
||||
- **国际化**: 中文支持
|
||||
|
||||
## 📡 API接口
|
||||
|
||||
### 硬件配置接口
|
||||
- `GET /api/configs/` - 获取硬件配置列表
|
||||
- `GET /api/configs/{id}/` - 获取特定配置详情
|
||||
|
||||
### 订单接口
|
||||
- `POST /api/orders/` - 创建订单
|
||||
- `GET /api/orders/{id}/` - 获取订单详情
|
||||
- `POST /api/orders/{id}/pay/` - 订单支付
|
||||
|
||||
### 支付接口
|
||||
- `POST /api/payments/initiate/` - 发起支付
|
||||
- `POST /api/payments/confirm/` - 确认支付
|
||||
|
||||
## 🎯 使用说明
|
||||
|
||||
### 推广码功能
|
||||
系统支持URL推广码参数,格式:`?ref=推广码`
|
||||
|
||||
### 支付流程
|
||||
1. 选择硬件配置
|
||||
2. 填写订单信息
|
||||
3. 发起支付请求
|
||||
4. 确认支付结果
|
||||
5. 订单完成
|
||||
|
||||
## 🔒 安全说明
|
||||
|
||||
- 生产环境请修改 `SECRET_KEY`
|
||||
- 配置HTTPS证书
|
||||
- 设置适当的CORS白名单
|
||||
- 定期备份数据库
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### 跨域问题
|
||||
确保后端CORS配置正确,开发环境可设置为允许所有来源。
|
||||
|
||||
### 数据库连接失败
|
||||
检查PostgreSQL服务状态和连接配置。
|
||||
|
||||
### 前端构建失败
|
||||
检查Node.js版本和依赖包完整性。
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
如有问题或建议,请通过以下方式联系:
|
||||
- 邮箱:support@Quant-Speed-ai.com
|
||||
- 电话:400-123-4567
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢以下开源项目的支持:
|
||||
- [React](https://reactjs.org/)
|
||||
- [Django](https://www.djangoproject.com/)
|
||||
- [Ant Design](https://ant.design/)
|
||||
- [Vite](https://vitejs.dev/)
|
||||
|
||||
---
|
||||
|
||||
**⭐ 如果这个项目对您有帮助,请给我们一个星标!**
|
||||
6
backend/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
.git
|
||||
.env
|
||||
db.sqlite3
|
||||
25
backend/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
# Use an official Python runtime as a parent image
|
||||
FROM python:3.13-slim
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies (needed for psycopg2 and others)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install python dependencies
|
||||
COPY requirements.txt /app/
|
||||
RUN pip install --upgrade pip && pip install -r requirements.txt
|
||||
|
||||
# Copy project
|
||||
COPY . /app/
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||
0
backend/config/__init__.py
Normal file
BIN
backend/config/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/config/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/config/__pycache__/settings.cpython-312.pyc
Normal file
BIN
backend/config/__pycache__/settings.cpython-313.pyc
Normal file
BIN
backend/config/__pycache__/urls.cpython-312.pyc
Normal file
BIN
backend/config/__pycache__/urls.cpython-313.pyc
Normal file
BIN
backend/config/__pycache__/wsgi.cpython-312.pyc
Normal file
BIN
backend/config/__pycache__/wsgi.cpython-313.pyc
Normal file
16
backend/config/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for config project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
128
backend/config/settings.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Django settings for config project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 6.0.1.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-9hwh_v44(3n)61g)tiwkvm1k0h&5c+u=68&z*!$e0ujpd-6^1o'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'corsheaders',
|
||||
'shop',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
||||
ROOT_URLCONF = 'config.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'config.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'market',
|
||||
'USER': 'market',
|
||||
'PASSWORD': '123market',
|
||||
'HOST': '6.6.6.66',
|
||||
'PORT': '5432',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'zh-hans'
|
||||
|
||||
TIME_ZONE = 'Asia/Shanghai'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
7
backend/config/urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include('shop.urls')),
|
||||
]
|
||||
16
backend/config/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for config project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
BIN
backend/db.sqlite3
Normal file
22
backend/manage.py
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
52
backend/populate_db.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import os
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
django.setup()
|
||||
|
||||
from shop.models import ESP32Config
|
||||
|
||||
def populate():
|
||||
# 清除旧数据,避免重复累积
|
||||
# 注意:在生产环境中慎用 delete
|
||||
ESP32Config.objects.all().delete()
|
||||
|
||||
configs = [
|
||||
{
|
||||
"name": "AI小智 Mini款",
|
||||
"chip_type": "ESP32-C3",
|
||||
"flash_size": 4,
|
||||
"ram_size": 1,
|
||||
"has_camera": False,
|
||||
"has_microphone": True,
|
||||
"price": 150.00,
|
||||
"description": "高性价比入门款,支持语音交互,小巧便携。"
|
||||
},
|
||||
{
|
||||
"name": "AI小智 V2款 (舵机版)",
|
||||
"chip_type": "ESP32-S3",
|
||||
"flash_size": 8,
|
||||
"ram_size": 2,
|
||||
"has_camera": False,
|
||||
"has_microphone": True,
|
||||
"price": 188.00,
|
||||
"description": "升级版性能,支持驱动舵机,适合机器人控制与运动交互。"
|
||||
},
|
||||
{
|
||||
"name": "AI小智 V3款 (视觉版)",
|
||||
"chip_type": "ESP32-S3",
|
||||
"flash_size": 16,
|
||||
"ram_size": 8,
|
||||
"has_camera": True,
|
||||
"has_microphone": True,
|
||||
"price": 250.00,
|
||||
"description": "旗舰视觉版,配备摄像头与高性能计算单元,支持视觉识别与复杂AI任务。"
|
||||
}
|
||||
]
|
||||
|
||||
for data in configs:
|
||||
config = ESP32Config.objects.create(**data)
|
||||
print(f"Created: {config.name} - ¥{config.price}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
populate()
|
||||
8
backend/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
asgiref==3.11.0
|
||||
Django==6.0.1
|
||||
django-cors-headers==4.9.0
|
||||
djangorestframework==3.16.1
|
||||
pillow==12.1.0
|
||||
psycopg2-binary==2.9.11
|
||||
qrcode==8.2
|
||||
sqlparse==0.5.5
|
||||
0
backend/shop/__init__.py
Normal file
BIN
backend/shop/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/admin.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/admin.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/apps.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/apps.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/models.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/models.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/serializers.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/urls.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/urls.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/views.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/views.cpython-313.pyc
Normal file
177
backend/shop/admin.py
Normal file
@@ -0,0 +1,177 @@
|
||||
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
|
||||
import qrcode
|
||||
from io import BytesIO
|
||||
import base64
|
||||
|
||||
# 自定义后台标题
|
||||
admin.site.site_header = "量迹AI硬件销售管理后台"
|
||||
admin.site.site_title = "量迹AI后台"
|
||||
admin.site.index_title = "欢迎使用量迹AI管理系统"
|
||||
|
||||
@admin.register(WeChatPayConfig)
|
||||
class WeChatPayConfigAdmin(admin.ModelAdmin):
|
||||
list_display = ('app_id', 'mch_id', 'is_active', 'notify_url')
|
||||
list_filter = ('is_active',)
|
||||
search_fields = ('app_id', 'mch_id')
|
||||
fieldsets = (
|
||||
('基本配置', {
|
||||
'fields': ('app_id', 'mch_id', 'is_active')
|
||||
}),
|
||||
('安全配置', {
|
||||
'fields': ('api_key', 'app_secret')
|
||||
}),
|
||||
('回调配置', {
|
||||
'fields': ('notify_url',)
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(ESP32Config)
|
||||
class ESP32ConfigAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone')
|
||||
list_filter = ('chip_type', 'has_camera')
|
||||
search_fields = ('name', 'description')
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('name', 'price', 'description')
|
||||
}),
|
||||
('硬件参数', {
|
||||
'fields': ('chip_type', 'flash_size', 'ram_size', 'has_camera', 'has_microphone')
|
||||
}),
|
||||
('详情页图片', {
|
||||
'fields': ('detail_image', 'detail_image_url'),
|
||||
'description': '图片上传和URL二选一,优先使用URL'
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(Service)
|
||||
class ServiceAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'created_at')
|
||||
search_fields = ('title', 'description')
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('title', 'description', 'color')
|
||||
}),
|
||||
('图标', {
|
||||
'fields': ('icon', 'icon_url'),
|
||||
'description': '图标上传和URL二选一,优先使用URL'
|
||||
}),
|
||||
('详情页图片', {
|
||||
'fields': ('detail_image', 'detail_image_url'),
|
||||
'description': '图片上传和URL二选一,优先使用URL'
|
||||
}),
|
||||
('详细内容', {
|
||||
'fields': ('features',)
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(ARService)
|
||||
class ARServiceAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'created_at')
|
||||
search_fields = ('title', 'description')
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('title', 'description')
|
||||
}),
|
||||
('封面/长图', {
|
||||
'fields': ('cover_image', 'cover_image_url'),
|
||||
'description': '图片上传和URL二选一,优先使用URL'
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(Salesperson)
|
||||
class SalespersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'code', 'total_sales', 'view_promotion_url')
|
||||
search_fields = ('name', 'code')
|
||||
readonly_fields = ('promotion_qr_code', 'promotion_url_display', 'total_sales_display')
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = super().get_queryset(request)
|
||||
queryset = queryset.annotate(
|
||||
_total_sales=Sum('orders__total_price', default=0)
|
||||
)
|
||||
return queryset
|
||||
|
||||
def total_sales(self, obj):
|
||||
# 仅计算已支付的订单
|
||||
paid_sales = obj.orders.filter(status='paid').aggregate(total=Sum('total_price'))['total']
|
||||
return f"¥{paid_sales or 0:.2f}"
|
||||
total_sales.short_description = "累计销售额 (已支付)"
|
||||
total_sales.admin_order_field = '_total_sales'
|
||||
|
||||
def total_sales_display(self, obj):
|
||||
return self.total_sales(obj)
|
||||
total_sales_display.short_description = "累计销售额 (已支付)"
|
||||
|
||||
def promotion_url(self, obj):
|
||||
# 假设前端部署在 localhost:5173,生产环境需配置
|
||||
base_url = "http://localhost:5173"
|
||||
return f"{base_url}/?ref={obj.code}"
|
||||
|
||||
def view_promotion_url(self, obj):
|
||||
url = self.promotion_url(obj)
|
||||
return format_html('<a href="{}" target="_blank">打开推广链接</a>', url)
|
||||
view_promotion_url.short_description = "推广链接"
|
||||
|
||||
def promotion_url_display(self, obj):
|
||||
return self.promotion_url(obj)
|
||||
promotion_url_display.short_description = "完整推广链接"
|
||||
|
||||
def promotion_qr_code(self, obj):
|
||||
if not obj.code:
|
||||
return "请先保存以生成二维码"
|
||||
|
||||
url = self.promotion_url(obj)
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=10,
|
||||
border=4,
|
||||
)
|
||||
qr.add_data(url)
|
||||
qr.make(fit=True)
|
||||
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
img_str = base64.b64encode(buffer.getvalue()).decode()
|
||||
|
||||
return format_html('<img src="data:image/png;base64,{}" width="200" height="200" />', img_str)
|
||||
|
||||
promotion_qr_code.short_description = "推广二维码"
|
||||
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('name', 'code')
|
||||
}),
|
||||
('推广工具', {
|
||||
'fields': ('promotion_url_display', 'promotion_qr_code')
|
||||
}),
|
||||
('业绩统计', {
|
||||
'fields': ('total_sales_display',)
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'customer_name', 'config', 'total_price', 'status', 'salesperson', 'created_at')
|
||||
list_filter = ('status', 'salesperson', 'created_at')
|
||||
search_fields = ('id', 'customer_name', 'phone_number', 'wechat_trade_no')
|
||||
readonly_fields = ('total_price', 'created_at', 'wechat_trade_no')
|
||||
|
||||
fieldsets = (
|
||||
('订单信息', {
|
||||
'fields': ('config', 'quantity', 'total_price', 'status', 'created_at')
|
||||
}),
|
||||
('客户信息', {
|
||||
'fields': ('customer_name', 'phone_number', 'shipping_address')
|
||||
}),
|
||||
('销售归属', {
|
||||
'fields': ('salesperson',)
|
||||
}),
|
||||
('支付信息', {
|
||||
'fields': ('wechat_trade_no',)
|
||||
}),
|
||||
)
|
||||
5
backend/shop/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ShopConfig(AppConfig):
|
||||
name = 'shop'
|
||||
50
backend/shop/migrations/0001_initial.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-02 04:06
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ESP32Config',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='配置名称')),
|
||||
('chip_type', models.CharField(help_text='例如: ESP32-S3, ESP32-C3', max_length=50, verbose_name='芯片型号')),
|
||||
('flash_size', models.IntegerField(default=4, verbose_name='Flash大小(MB)')),
|
||||
('ram_size', models.IntegerField(default=2, verbose_name='PSRAM大小(MB)')),
|
||||
('has_camera', models.BooleanField(default=False, verbose_name='是否包含摄像头')),
|
||||
('has_microphone', models.BooleanField(default=False, verbose_name='是否包含麦克风')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='价格')),
|
||||
('description', models.TextField(blank=True, verbose_name='描述')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '硬件配置',
|
||||
'verbose_name_plural': '硬件配置列表',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Order',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', models.IntegerField(default=1, verbose_name='数量')),
|
||||
('total_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='总价')),
|
||||
('status', models.CharField(choices=[('pending', '待支付'), ('paid', '已支付'), ('shipped', '已发货'), ('cancelled', '已取消')], default='pending', max_length=20, verbose_name='订单状态')),
|
||||
('wechat_trade_no', models.CharField(blank=True, max_length=100, null=True, verbose_name='微信支付单号')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.esp32config', verbose_name='所选配置')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '订单',
|
||||
'verbose_name_plural': '订单列表',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-02 04:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='customer_name',
|
||||
field=models.CharField(default='', max_length=100, verbose_name='收货人姓名'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='phone_number',
|
||||
field=models.CharField(default='', max_length=20, verbose_name='联系电话'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='shipping_address',
|
||||
field=models.TextField(default='', verbose_name='发货地址'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-02 04:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0002_order_customer_name_order_phone_number_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Salesperson',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, verbose_name='销售员姓名')),
|
||||
('code', models.CharField(help_text='唯一的推广标识码,如: zhangsan01', max_length=20, unique=True, verbose_name='推广码')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '销售员',
|
||||
'verbose_name_plural': '销售员管理',
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='esp32config',
|
||||
options={'verbose_name': '硬件配置 (小智参数)', 'verbose_name_plural': '硬件配置 (小智参数)'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='salesperson',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='shop.salesperson', verbose_name='所属销售员'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 5.2.10 on 2026-02-02 04:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0003_salesperson_alter_esp32config_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WeChatPayConfig',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('app_id', models.CharField(max_length=50, verbose_name='AppID')),
|
||||
('mch_id', models.CharField(max_length=50, verbose_name='商户号(MchID)')),
|
||||
('api_key', models.CharField(max_length=100, verbose_name='API密钥(Key)')),
|
||||
('app_secret', models.CharField(blank=True, max_length=100, null=True, verbose_name='AppSecret')),
|
||||
('notify_url', models.URLField(verbose_name='回调通知地址')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '微信支付配置',
|
||||
'verbose_name_plural': '微信支付配置',
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='esp32config',
|
||||
name='id',
|
||||
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='id',
|
||||
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='salesperson',
|
||||
name='id',
|
||||
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-02 05:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0004_wechatpayconfig_alter_esp32config_id_alter_order_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Service',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=100, verbose_name='服务名称')),
|
||||
('icon', models.ImageField(upload_to='services/icons/', verbose_name='图标')),
|
||||
('description', models.TextField(verbose_name='简介')),
|
||||
('features', models.TextField(help_text='每行一个特性', verbose_name='特性列表')),
|
||||
('color', models.CharField(default='#00f0ff', max_length=20, verbose_name='主题色')),
|
||||
('detail_image', models.ImageField(blank=True, null=True, upload_to='services/details/', verbose_name='详情页长图')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'AI服务',
|
||||
'verbose_name_plural': 'AI服务管理',
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='esp32config',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='salesperson',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='wechatpayconfig',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-02 05:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0005_service_alter_esp32config_id_alter_order_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ARService',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=100, verbose_name='体验名称')),
|
||||
('description', models.TextField(verbose_name='简介')),
|
||||
('cover_image', models.ImageField(blank=True, null=True, upload_to='ar/covers/', verbose_name='封面/长图 (上传)')),
|
||||
('cover_image_url', models.URLField(blank=True, null=True, verbose_name='封面/长图 (URL)')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'AR体验',
|
||||
'verbose_name_plural': 'AR体验管理',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='esp32config',
|
||||
name='detail_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='products/details/', verbose_name='详情页长图 (上传)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='esp32config',
|
||||
name='detail_image_url',
|
||||
field=models.URLField(blank=True, help_text='如果填写了URL,将优先使用URL', null=True, verbose_name='详情页长图 (URL)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='service',
|
||||
name='detail_image_url',
|
||||
field=models.URLField(blank=True, null=True, verbose_name='详情页长图 (URL)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='service',
|
||||
name='icon_url',
|
||||
field=models.URLField(blank=True, null=True, verbose_name='图标 (URL)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='service',
|
||||
name='detail_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='services/details/', verbose_name='详情页长图 (上传)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='service',
|
||||
name='icon',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='services/icons/', verbose_name='图标 (上传)'),
|
||||
),
|
||||
]
|
||||
0
backend/shop/migrations/__init__.py
Normal file
BIN
backend/shop/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
backend/shop/migrations/__pycache__/0001_initial.cpython-313.pyc
Normal file
BIN
backend/shop/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/shop/migrations/__pycache__/__init__.cpython-313.pyc
Normal file
147
backend/shop/models.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from django.db import models
|
||||
from django.utils.html import format_html
|
||||
import qrcode
|
||||
from io import BytesIO
|
||||
import base64
|
||||
|
||||
class ESP32Config(models.Model):
|
||||
"""
|
||||
ESP32 硬件配置选项模型
|
||||
用于定义可售卖的硬件参数
|
||||
"""
|
||||
name = models.CharField(max_length=100, verbose_name="配置名称")
|
||||
chip_type = models.CharField(max_length=50, verbose_name="芯片型号", help_text="例如: ESP32-S3, ESP32-C3")
|
||||
flash_size = models.IntegerField(verbose_name="Flash大小(MB)", default=4)
|
||||
ram_size = models.IntegerField(verbose_name="PSRAM大小(MB)", default=2)
|
||||
has_camera = models.BooleanField(default=False, verbose_name="是否包含摄像头")
|
||||
has_microphone = models.BooleanField(default=False, verbose_name="是否包含麦克风")
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="价格")
|
||||
description = models.TextField(verbose_name="描述", blank=True)
|
||||
detail_image = models.ImageField(upload_to='products/details/', blank=True, null=True, verbose_name="详情页长图 (上传)")
|
||||
detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)", help_text="如果填写了URL,将优先使用URL")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} - ¥{self.price}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "硬件配置 (小智参数)"
|
||||
verbose_name_plural = "硬件配置 (小智参数)"
|
||||
|
||||
|
||||
class Salesperson(models.Model):
|
||||
"""
|
||||
销售人员模型
|
||||
"""
|
||||
name = models.CharField(max_length=50, verbose_name="销售员姓名")
|
||||
code = models.CharField(max_length=20, unique=True, verbose_name="推广码", help_text="唯一的推广标识码,如: zhangsan01")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.code})"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "销售员"
|
||||
verbose_name_plural = "销售员管理"
|
||||
|
||||
|
||||
class WeChatPayConfig(models.Model):
|
||||
"""
|
||||
微信支付配置模型
|
||||
"""
|
||||
app_id = models.CharField(max_length=50, verbose_name="AppID")
|
||||
mch_id = models.CharField(max_length=50, verbose_name="商户号(MchID)")
|
||||
api_key = models.CharField(max_length=100, verbose_name="API密钥(Key)")
|
||||
app_secret = models.CharField(max_length=100, verbose_name="AppSecret", blank=True, null=True)
|
||||
notify_url = models.URLField(verbose_name="回调通知地址")
|
||||
is_active = models.BooleanField(default=True, verbose_name="是否启用")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "微信支付配置"
|
||||
verbose_name_plural = "微信支付配置"
|
||||
|
||||
def __str__(self):
|
||||
return f"微信支付配置 ({'启用' if self.is_active else '禁用'})"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# 确保只有一个启用的配置
|
||||
if self.is_active:
|
||||
WeChatPayConfig.objects.filter(is_active=True).exclude(id=self.id).update(is_active=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
"""
|
||||
订单模型
|
||||
记录用户的购买请求和支付状态
|
||||
"""
|
||||
STATUS_CHOICES = (
|
||||
('pending', '待支付'),
|
||||
('paid', '已支付'),
|
||||
('shipped', '已发货'),
|
||||
('cancelled', '已取消'),
|
||||
)
|
||||
|
||||
config = models.ForeignKey(ESP32Config, on_delete=models.CASCADE, verbose_name="所选配置")
|
||||
quantity = models.IntegerField(default=1, verbose_name="数量")
|
||||
total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="总价")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="订单状态")
|
||||
|
||||
# 销售归属
|
||||
salesperson = models.ForeignKey(Salesperson, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属销售员", related_name='orders')
|
||||
|
||||
# 用户信息
|
||||
customer_name = models.CharField(max_length=100, verbose_name="收货人姓名", default="")
|
||||
phone_number = models.CharField(max_length=20, verbose_name="联系电话", default="")
|
||||
shipping_address = models.TextField(verbose_name="发货地址", default="")
|
||||
|
||||
# 微信支付相关字段
|
||||
wechat_trade_no = models.CharField(max_length=100, blank=True, null=True, verbose_name="微信支付单号")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
def __str__(self):
|
||||
return f"Order #{self.id} - {self.customer_name} - {self.status}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "订单"
|
||||
verbose_name_plural = "订单列表"
|
||||
|
||||
|
||||
class Service(models.Model):
|
||||
"""
|
||||
AI服务项目模型
|
||||
"""
|
||||
title = models.CharField(max_length=100, verbose_name="服务名称")
|
||||
icon = models.ImageField(upload_to='services/icons/', blank=True, null=True, verbose_name="图标 (上传)")
|
||||
icon_url = models.URLField(blank=True, null=True, verbose_name="图标 (URL)")
|
||||
description = models.TextField(verbose_name="简介")
|
||||
features = models.TextField(verbose_name="特性列表", help_text="每行一个特性")
|
||||
color = models.CharField(max_length=20, default="#00f0ff", verbose_name="主题色")
|
||||
detail_image = models.ImageField(upload_to='services/details/', blank=True, null=True, verbose_name="详情页长图 (上传)")
|
||||
detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = "AI服务"
|
||||
verbose_name_plural = "AI服务管理"
|
||||
|
||||
|
||||
class ARService(models.Model):
|
||||
"""
|
||||
AR体验服务模型
|
||||
"""
|
||||
title = models.CharField(max_length=100, verbose_name="体验名称")
|
||||
description = models.TextField(verbose_name="简介")
|
||||
cover_image = models.ImageField(upload_to='ar/covers/', blank=True, null=True, verbose_name="封面/长图 (上传)")
|
||||
cover_image_url = models.URLField(blank=True, null=True, verbose_name="封面/长图 (URL)")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = "AR体验"
|
||||
verbose_name_plural = "AR体验管理"
|
||||
108
backend/shop/serializers.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from rest_framework import serializers
|
||||
from .models import ESP32Config, Order, Salesperson, Service, ARService
|
||||
|
||||
class ServiceSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
AI服务序列化器
|
||||
"""
|
||||
features_list = serializers.SerializerMethodField()
|
||||
display_icon = serializers.SerializerMethodField()
|
||||
display_detail_image = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Service
|
||||
fields = '__all__'
|
||||
|
||||
def get_features_list(self, obj):
|
||||
if obj.features:
|
||||
return [line.strip() for line in obj.features.split('\n') if line.strip()]
|
||||
return []
|
||||
|
||||
def get_display_icon(self, obj):
|
||||
if obj.icon_url:
|
||||
return obj.icon_url
|
||||
if obj.icon:
|
||||
return obj.icon.url
|
||||
return None
|
||||
|
||||
def get_display_detail_image(self, obj):
|
||||
if obj.detail_image_url:
|
||||
return obj.detail_image_url
|
||||
if obj.detail_image:
|
||||
return obj.detail_image.url
|
||||
return None
|
||||
|
||||
class ARServiceSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
AR服务序列化器
|
||||
"""
|
||||
display_cover_image = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ARService
|
||||
fields = '__all__'
|
||||
|
||||
def get_display_cover_image(self, obj):
|
||||
if obj.cover_image_url:
|
||||
return obj.cover_image_url
|
||||
if obj.cover_image:
|
||||
return obj.cover_image.url
|
||||
return None
|
||||
|
||||
class ESP32ConfigSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
ESP32配置序列化器
|
||||
"""
|
||||
display_detail_image = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ESP32Config
|
||||
fields = '__all__'
|
||||
|
||||
def get_display_detail_image(self, obj):
|
||||
if obj.detail_image_url:
|
||||
return obj.detail_image_url
|
||||
if obj.detail_image:
|
||||
return obj.detail_image.url
|
||||
return None
|
||||
|
||||
|
||||
class OrderSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
订单序列化器
|
||||
"""
|
||||
config_name = serializers.CharField(source='config.name', read_only=True)
|
||||
# 接收前端传来的 ref_code,用于查找 Salesperson
|
||||
ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['id', 'config', 'config_name', 'quantity', 'total_price', 'status', 'created_at', 'wechat_trade_no',
|
||||
'customer_name', 'phone_number', 'shipping_address', 'ref_code']
|
||||
read_only_fields = ['total_price', 'status', 'wechat_trade_no', 'created_at']
|
||||
extra_kwargs = {
|
||||
'customer_name': {'required': True},
|
||||
'phone_number': {'required': True},
|
||||
'shipping_address': {'required': True},
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
重写创建方法,自动计算总价并关联销售员
|
||||
"""
|
||||
config = validated_data.get('config')
|
||||
quantity = validated_data.get('quantity', 1)
|
||||
ref_code = validated_data.pop('ref_code', None)
|
||||
|
||||
validated_data['total_price'] = config.price * quantity
|
||||
|
||||
# 尝试关联销售员
|
||||
if ref_code:
|
||||
try:
|
||||
salesperson = Salesperson.objects.get(code=ref_code)
|
||||
validated_data['salesperson'] = salesperson
|
||||
except Salesperson.DoesNotExist:
|
||||
# 如果找不到对应的销售员,忽略该推广码,仍继续创建订单(算作自然流量)
|
||||
pass
|
||||
|
||||
return super().create(validated_data)
|
||||
151
backend/shop/templates/shop/order_check.html
Normal file
@@ -0,0 +1,151 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>订单查询 - 量迹AI硬件</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
#result {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.order-card {
|
||||
border: 1px solid #eee;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.status {
|
||||
font-weight: bold;
|
||||
}
|
||||
.status-paid { color: green; }
|
||||
.status-pending { color: orange; }
|
||||
.status-shipped { color: blue; }
|
||||
.error { color: red; text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>订单状态查询</h1>
|
||||
<div class="form-group">
|
||||
<label for="phone">请输入手机号码查询:</label>
|
||||
<input type="tel" id="phone" placeholder="请输入下单时填写的手机号" required>
|
||||
</div>
|
||||
<button onclick="searchOrders()">查询订单</button>
|
||||
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function searchOrders() {
|
||||
const phone = document.getElementById('phone').value;
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
if (!phone) {
|
||||
alert('请输入手机号码');
|
||||
return;
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = '<p style="text-align:center">查询中...</p>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/orders/lookup/?phone=${phone}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (data.length === 0) {
|
||||
resultDiv.innerHTML = '<p class="error">未找到相关订单</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
data.forEach(order => {
|
||||
const statusMap = {
|
||||
'pending': '待支付',
|
||||
'paid': '已支付',
|
||||
'shipped': '已发货',
|
||||
'cancelled': '已取消'
|
||||
};
|
||||
const statusText = statusMap[order.status] || order.status;
|
||||
const statusClass = `status-${order.status}`;
|
||||
|
||||
html += `
|
||||
<div class="order-card">
|
||||
<div class="order-header">
|
||||
<span>订单号: ${order.id}</span>
|
||||
<span class="status ${statusClass}">${statusText}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>商品:</strong> ${order.config_name || '未命名配置'}</p>
|
||||
<p><strong>数量:</strong> ${order.quantity}</p>
|
||||
<p><strong>总价:</strong> ¥${order.total_price}</p>
|
||||
<p><strong>下单时间:</strong> ${new Date(order.created_at).toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
resultDiv.innerHTML = html;
|
||||
} else {
|
||||
resultDiv.innerHTML = `<p class="error">${data.error || '查询失败'}</p>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
resultDiv.innerHTML = '<p class="error">网络错误,请稍后重试</p>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
3
backend/shop/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
14
backend/shop/urls.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import ESP32ConfigViewSet, OrderViewSet, order_check_view, ServiceViewSet, ARServiceViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'configs', ESP32ConfigViewSet)
|
||||
router.register(r'orders', OrderViewSet)
|
||||
router.register(r'services', ServiceViewSet)
|
||||
router.register(r'ar', ARServiceViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
path('page/check-order/', order_check_view, name='check-order-page'),
|
||||
]
|
||||
125
backend/shop/views.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.shortcuts import render
|
||||
from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService
|
||||
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer
|
||||
|
||||
class ARServiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
AR服务列表和详情
|
||||
"""
|
||||
queryset = ARService.objects.all().order_by('-created_at')
|
||||
serializer_class = ARServiceSerializer
|
||||
import uuid
|
||||
import time
|
||||
import hashlib
|
||||
|
||||
def order_check_view(request):
|
||||
"""
|
||||
订单查询页面视图
|
||||
"""
|
||||
return render(request, 'shop/order_check.html')
|
||||
|
||||
class ServiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
AI服务列表和详情
|
||||
"""
|
||||
queryset = Service.objects.all().order_by('-created_at')
|
||||
serializer_class = ServiceSerializer
|
||||
|
||||
class ESP32ConfigViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
提供ESP32配置选项的列表和详情
|
||||
"""
|
||||
queryset = ESP32Config.objects.all()
|
||||
serializer_class = ESP32ConfigSerializer
|
||||
|
||||
|
||||
class OrderViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
订单管理视图集
|
||||
支持创建订单和查询订单状态
|
||||
"""
|
||||
queryset = Order.objects.all()
|
||||
serializer_class = OrderSerializer
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def lookup(self, request):
|
||||
"""
|
||||
根据电话号码查询订单状态
|
||||
URL: /api/orders/lookup/?phone=13800138000
|
||||
"""
|
||||
phone = request.query_params.get('phone')
|
||||
if not phone:
|
||||
return Response({'error': '请提供电话号码'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 简单校验
|
||||
orders = Order.objects.filter(phone_number=phone).order_by('-created_at')
|
||||
serializer = self.get_serializer(orders, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def initiate_payment(self, request, pk=None):
|
||||
"""
|
||||
发起支付请求
|
||||
获取微信支付配置并生成签名
|
||||
"""
|
||||
order = self.get_object()
|
||||
|
||||
if order.status == 'paid':
|
||||
return Response({'error': '订单已支付'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 获取微信支付配置
|
||||
wechat_config = WeChatPayConfig.objects.filter(is_active=True).first()
|
||||
if not wechat_config:
|
||||
# 如果没有配置,为了演示方便,回退到模拟数据,或者报错
|
||||
# 这里我们报错提示需要在后台配置
|
||||
return Response({'error': '支付系统维护中 (未配置支付参数)'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
|
||||
# 构造支付参数
|
||||
# 注意:实际生产环境必须在此处调用微信【统一下单】接口获取真实的 prepay_id
|
||||
# 这里为了演示完整流程,我们使用配置中的参数生成合法的签名结构,但 prepay_id 是模拟的
|
||||
|
||||
app_id = wechat_config.app_id
|
||||
timestamp = str(int(time.time()))
|
||||
nonce_str = str(uuid.uuid4()).replace('-', '')
|
||||
|
||||
# 模拟的 prepay_id
|
||||
prepay_id = f"wx{str(uuid.uuid4()).replace('-', '')}"
|
||||
package = f"prepay_id={prepay_id}"
|
||||
sign_type = 'MD5'
|
||||
|
||||
# 生成签名 (WeChat Pay V2 MD5 Signature)
|
||||
# 签名步骤:
|
||||
# 1. 设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序)
|
||||
# 2. 使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA
|
||||
# 3. 在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写
|
||||
|
||||
stringA = f"appId={app_id}&nonceStr={nonce_str}&package={package}&signType={sign_type}&timeStamp={timestamp}"
|
||||
string_sign_temp = f"{stringA}&key={wechat_config.api_key}"
|
||||
pay_sign = hashlib.md5(string_sign_temp.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
payment_params = {
|
||||
'appId': app_id,
|
||||
'timeStamp': timestamp,
|
||||
'nonceStr': nonce_str,
|
||||
'package': package,
|
||||
'signType': sign_type,
|
||||
'paySign': pay_sign,
|
||||
'orderId': order.id,
|
||||
'amount': str(order.total_price)
|
||||
}
|
||||
|
||||
return Response(payment_params)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def confirm_payment(self, request, pk=None):
|
||||
"""
|
||||
模拟支付成功回调/确认
|
||||
"""
|
||||
order = self.get_object()
|
||||
order.status = 'paid'
|
||||
order.wechat_trade_no = f"WX_{str(uuid.uuid4())[:18]}"
|
||||
order.save()
|
||||
return Response({'status': 'success', 'message': '支付成功'})
|
||||
42
docker-compose.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=market
|
||||
- POSTGRES_USER=market
|
||||
- POSTGRES_PASSWORD=123market
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
command: sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- DB_NAME=market
|
||||
- DB_USER=market
|
||||
- DB_PASSWORD=123market
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
ports:
|
||||
- "5173:5173"
|
||||
environment:
|
||||
- VITE_API_URL=http://localhost:8000/api
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
4
frontend/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.env
|
||||
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
18
frontend/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
# Use an official Node runtime as a parent image
|
||||
FROM node:18-alpine
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
|
||||
# Copy project files
|
||||
COPY . .
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 5173
|
||||
|
||||
# Start the application
|
||||
CMD ["npm", "run", "dev", "--", "--host"]
|
||||
16
frontend/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
29
frontend/eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/Quant-Speed_logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4886
frontend/package-lock.json
generated
Normal file
34
frontend/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"antd": "^6.2.2",
|
||||
"axios": "^1.13.4",
|
||||
"framer-motion": "^12.29.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"three": "^0.182.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"vite": "^7.2.4"
|
||||
}
|
||||
}
|
||||
253
frontend/public/3dV2/xiaoliangV2.mtl
Normal file
@@ -0,0 +1,253 @@
|
||||
# Designed by EasyEDA Pro
|
||||
newmtl mtl1
|
||||
Ka 0.25 0.25 0.25
|
||||
Kd 0.25 0.25 0.25
|
||||
Ks 0.13 0.13 0.13
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl2
|
||||
Ka 0.75 0.75 0.75
|
||||
Kd 0.75 0.75 0.75
|
||||
Ks 0.38 0.38 0.38
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl3
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 1.00 1.00 1.00
|
||||
Ks 0.50 0.50 0.50
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl4
|
||||
Ka 0.54 0.35 0.34
|
||||
Kd 0.54 0.35 0.34
|
||||
Ks 0.38 0.24 0.24
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl5
|
||||
Ka 0.43 0.35 0.37
|
||||
Kd 0.43 0.35 0.37
|
||||
Ks 0.30 0.25 0.26
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl6
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 1.00 1.00 1.00
|
||||
Ks 0.30 0.30 0.30
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl7
|
||||
Ka 0.11 0.11 0.11
|
||||
Kd 0.11 0.11 0.11
|
||||
Ks 0.08 0.08 0.08
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl8
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 1.00 1.00 1.00
|
||||
Ks 0.88 0.88 0.88
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl9
|
||||
Ka 0.78 0.76 0.74
|
||||
Kd 0.78 0.76 0.74
|
||||
Ks 0.39 0.38 0.37
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl10
|
||||
Ka 0.59 0.46 0.00
|
||||
Kd 0.59 0.46 0.00
|
||||
Ks 0.29 0.23 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl11
|
||||
Ka 0.85 0.85 0.85
|
||||
Kd 0.85 0.85 0.85
|
||||
Ks 0.43 0.43 0.43
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl12
|
||||
Ka 0.75 0.75 0.00
|
||||
Kd 0.75 0.75 0.00
|
||||
Ks 0.38 0.38 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl13
|
||||
Ka 0.00 0.00 0.00
|
||||
Kd 0.00 0.00 0.00
|
||||
Ks 0.00 0.00 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl14
|
||||
Ka 0.50 0.50 0.50
|
||||
Kd 0.50 0.50 0.50
|
||||
Ks 0.25 0.25 0.25
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl15
|
||||
Ka 0.50 0.25 0.00
|
||||
Kd 0.50 0.25 0.00
|
||||
Ks 0.44 0.22 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl16
|
||||
Ka 1.00 1.00 0.50
|
||||
Kd 1.00 1.00 0.50
|
||||
Ks 0.50 0.50 0.25
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl17
|
||||
Ka 1.00 0.89 0.81
|
||||
Kd 1.00 0.89 0.81
|
||||
Ks 0.50 0.44 0.40
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl18
|
||||
Ka 0.25 0.25 0.25
|
||||
Kd 0.25 0.25 0.25
|
||||
Ks 0.07 0.07 0.07
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl19
|
||||
Ka 0.64 0.62 0.60
|
||||
Kd 0.45 0.43 0.42
|
||||
Ks 0.03 0.03 0.03
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl20
|
||||
Ka 0.77 0.77 0.77
|
||||
Kd 0.77 0.77 0.77
|
||||
Ks 0.62 0.62 0.62
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl21
|
||||
Ka 0.50 0.25 0.25
|
||||
Kd 0.50 0.25 0.25
|
||||
Ks 0.44 0.22 0.22
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl22
|
||||
Ka 0.00 0.50 0.00
|
||||
Kd 0.00 0.50 0.00
|
||||
Ks 0.00 0.25 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl23
|
||||
Ka 0.65 0.62 0.59
|
||||
Kd 0.65 0.62 0.59
|
||||
Ks 0.45 0.44 0.41
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl24
|
||||
Ka 0.50 0.50 0.00
|
||||
Kd 0.50 0.50 0.00
|
||||
Ks 0.25 0.25 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl25
|
||||
Ka 1.00 0.92 0.81
|
||||
Kd 1.00 0.92 0.81
|
||||
Ks 0.50 0.46 0.40
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl26
|
||||
Ka 0.07 0.75 0.00
|
||||
Kd 0.05 0.60 0.00
|
||||
Ks 0.07 0.75 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl27
|
||||
Ka 0.84 0.82 0.75
|
||||
Kd 0.84 0.82 0.75
|
||||
Ks 0.42 0.41 0.38
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl28
|
||||
Ka 0.75 0.75 0.75
|
||||
Kd 0.60 0.60 0.60
|
||||
Ks 0.75 0.75 0.75
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl29
|
||||
Ka 0.97 0.88 0.60
|
||||
Kd 0.97 0.88 0.60
|
||||
Ks 0.68 0.62 0.42
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl30
|
||||
Ka 0.54 0.54 0.54
|
||||
Kd 1.00 1.00 1.00
|
||||
Ks 0.48 0.48 0.48
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl31
|
||||
Ka 0.27 0.26 0.26
|
||||
Kd 0.52 0.51 0.51
|
||||
Ks 0.48 0.48 0.48
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl32
|
||||
Ka 0.46 0.33 0.07
|
||||
Kd 0.87 0.63 0.17
|
||||
Ks 0.48 0.48 0.48
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl33
|
||||
Ka 0.46 0.31 0.17
|
||||
Kd 0.86 0.60 0.34
|
||||
Ks 0.48 0.48 0.48
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl34
|
||||
Ka 0.26 0.28 0.28
|
||||
Kd 0.51 0.53 0.55
|
||||
Ks 0.48 0.48 0.48
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl35
|
||||
Ka 0.12 0.12 0.12
|
||||
Kd 0.25 0.25 0.25
|
||||
Ks 0.48 0.48 0.48
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl36
|
||||
Ka 0.09 0.09 0.09
|
||||
Kd 0.20 0.20 0.20
|
||||
Ks 0.48 0.48 0.48
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl37
|
||||
Ka 0.01 0.01 0.01
|
||||
Kd 0.03 0.03 0.05
|
||||
Ks 0.48 0.48 0.48
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl38
|
||||
Ka 0.33 0.33 0.33
|
||||
Kd 0.63 0.63 0.63
|
||||
Ks 0.48 0.48 0.48
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl39
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 1.00 1.00 1.00
|
||||
Ks 1.00 1.00 1.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl40
|
||||
Ka 0.00 0.33 0.65
|
||||
Kd 0.00 0.33 0.65
|
||||
Ks 0.00 0.33 0.65
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl41
|
||||
Ka 0.62 0.62 0.36
|
||||
Kd 0.62 0.62 0.36
|
||||
Ks 0.62 0.62 0.36
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl42
|
||||
Ka 0.00 0.15 0.36
|
||||
Kd 0.00 0.15 0.36
|
||||
Ks 0.00 0.15 0.36
|
||||
d 1.00
|
||||
endmtl
|
||||
306612
frontend/public/3dV2/xiaoliangV2.obj
Normal file
301
frontend/public/3dmimi/3D_PCB_V3-mini.mtl
Normal file
@@ -0,0 +1,301 @@
|
||||
# Designed by EasyEDA Pro
|
||||
newmtl mtl1
|
||||
Ka 0.22 0.22 0.22
|
||||
Kd 0.22 0.22 0.22
|
||||
Ks 0.07 0.07 0.07
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl2
|
||||
Ka 0.65 0.65 0.65
|
||||
Kd 0.45 0.45 0.45
|
||||
Ks 0.03 0.03 0.03
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl3
|
||||
Ka 0.85 0.85 0.85
|
||||
Kd 0.85 0.85 0.85
|
||||
Ks 0.42 0.42 0.42
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl4
|
||||
Ka 0.30 0.30 0.30
|
||||
Kd 0.21 0.21 0.21
|
||||
Ks 0.02 0.02 0.02
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl5
|
||||
Ka 0.67 0.67 0.67
|
||||
Kd 0.67 0.67 0.67
|
||||
Ks 0.47 0.47 0.47
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl6
|
||||
Ka 0.83 0.67 0.13
|
||||
Kd 0.83 0.67 0.13
|
||||
Ks 0.83 0.67 0.13
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl7
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 1.00 1.00 1.00
|
||||
Ks 0.88 0.88 0.88
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl8
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 1.00 1.00 1.00
|
||||
Ks 0.50 0.50 0.50
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl9
|
||||
Ka 0.83 0.67 0.13
|
||||
Kd 0.83 0.67 0.13
|
||||
Ks 0.67 0.54 0.10
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl10
|
||||
Ka 0.22 0.22 0.22
|
||||
Kd 0.22 0.22 0.22
|
||||
Ks 0.11 0.11 0.11
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl11
|
||||
Ka 0.75 0.75 0.75
|
||||
Kd 0.75 0.75 0.75
|
||||
Ks 0.38 0.38 0.38
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl12
|
||||
Ka 0.65 0.62 0.59
|
||||
Kd 0.65 0.62 0.59
|
||||
Ks 0.45 0.44 0.41
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl13
|
||||
Ka 0.16 0.16 0.16
|
||||
Kd 0.11 0.11 0.11
|
||||
Ks 0.01 0.01 0.01
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl14
|
||||
Ka 0.97 0.97 0.97
|
||||
Kd 0.68 0.68 0.68
|
||||
Ks 0.05 0.05 0.05
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl15
|
||||
Ka 0.25 0.25 0.25
|
||||
Kd 0.25 0.25 0.25
|
||||
Ks 0.13 0.13 0.13
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl16
|
||||
Ka 0.50 0.50 0.50
|
||||
Kd 0.35 0.35 0.35
|
||||
Ks 0.02 0.02 0.02
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl17
|
||||
Ka 0.25 0.25 0.25
|
||||
Kd 0.25 0.25 0.25
|
||||
Ks 0.07 0.07 0.07
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl18
|
||||
Ka 0.64 0.62 0.60
|
||||
Kd 0.45 0.43 0.42
|
||||
Ks 0.03 0.03 0.03
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl19
|
||||
Ka 0.77 0.77 0.77
|
||||
Kd 0.77 0.77 0.77
|
||||
Ks 0.62 0.62 0.62
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl20
|
||||
Ka 0.50 0.50 0.50
|
||||
Kd 0.50 0.50 0.50
|
||||
Ks 0.25 0.25 0.25
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl21
|
||||
Ka 0.00 0.50 0.00
|
||||
Kd 0.00 0.50 0.00
|
||||
Ks 0.00 0.25 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl22
|
||||
Ka 0.00 0.00 0.00
|
||||
Kd 0.00 0.00 0.00
|
||||
Ks 0.00 0.00 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl23
|
||||
Ka 1.00 1.00 0.00
|
||||
Kd 1.00 1.00 0.00
|
||||
Ks 0.50 0.50 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl24
|
||||
Ka 0.50 0.25 0.00
|
||||
Kd 0.50 0.25 0.00
|
||||
Ks 0.44 0.22 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl25
|
||||
Ka 0.97 0.88 0.60
|
||||
Kd 0.97 0.88 0.60
|
||||
Ks 0.68 0.62 0.42
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl26
|
||||
Ka 0.11 0.11 0.11
|
||||
Kd 0.11 0.11 0.11
|
||||
Ks 0.08 0.08 0.08
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl27
|
||||
Ka 0.41 0.41 0.41
|
||||
Kd 0.41 0.41 0.41
|
||||
Ks 0.20 0.20 0.20
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl28
|
||||
Ka 0.79 0.82 0.93
|
||||
Kd 0.79 0.82 0.93
|
||||
Ks 0.40 0.41 0.47
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl29
|
||||
Ka 0.65 0.65 0.65
|
||||
Kd 0.65 0.65 0.65
|
||||
Ks 0.32 0.32 0.32
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl30
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 1.00 1.00 1.00
|
||||
Ks 0.30 0.30 0.30
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl31
|
||||
Ka 0.51 0.42 0.25
|
||||
Kd 0.95 0.80 0.49
|
||||
Ks 0.48 0.48 0.48
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl32
|
||||
Ka 1.00 0.93 0.62
|
||||
Kd 1.00 0.93 0.62
|
||||
Ks 0.50 0.47 0.31
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl33
|
||||
Ka 0.85 0.85 0.85
|
||||
Kd 0.85 0.85 0.85
|
||||
Ks 0.43 0.43 0.43
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl34
|
||||
Ka 0.59 0.46 0.00
|
||||
Kd 0.59 0.46 0.00
|
||||
Ks 0.29 0.23 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl35
|
||||
Ka 0.11 0.11 0.11
|
||||
Kd 0.11 0.11 0.11
|
||||
Ks 0.06 0.06 0.06
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl36
|
||||
Ka 0.82 0.82 0.82
|
||||
Kd 0.82 0.82 0.82
|
||||
Ks 0.41 0.41 0.41
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl37
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 0.88 0.88 0.88
|
||||
Ks 0.75 0.75 0.75
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl38
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 0.25 0.25 0.25
|
||||
Ks 0.75 0.75 0.75
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl39
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 0.76 0.76 0.76
|
||||
Ks 0.75 0.75 0.75
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl40
|
||||
Ka 1.00 0.50 0.00
|
||||
Kd 1.00 0.50 0.00
|
||||
Ks 0.88 0.44 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl41
|
||||
Ka 0.00 1.00 0.00
|
||||
Kd 0.00 1.00 0.00
|
||||
Ks 0.00 0.50 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl42
|
||||
Ka 0.67 0.70 0.77
|
||||
Kd 0.67 0.70 0.77
|
||||
Ks 0.33 0.35 0.38
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl43
|
||||
Ka 0.30 0.30 0.30
|
||||
Kd 0.30 0.30 0.30
|
||||
Ks 0.09 0.09 0.09
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl44
|
||||
Ka 0.84 0.82 0.75
|
||||
Kd 0.84 0.82 0.75
|
||||
Ks 0.42 0.41 0.38
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl45
|
||||
Ka 0.10 0.10 0.10
|
||||
Kd 0.10 0.10 0.10
|
||||
Ks 0.04 0.04 0.04
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl46
|
||||
Ka 0.86 0.86 0.86
|
||||
Kd 0.51 0.51 0.51
|
||||
Ks 0.35 0.35 0.35
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl47
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 1.00 1.00 1.00
|
||||
Ks 1.00 1.00 1.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl48
|
||||
Ka 0.00 0.33 0.65
|
||||
Kd 0.00 0.33 0.65
|
||||
Ks 0.00 0.33 0.65
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl49
|
||||
Ka 0.62 0.62 0.36
|
||||
Kd 0.62 0.62 0.36
|
||||
Ks 0.62 0.62 0.36
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl50
|
||||
Ka 0.00 0.15 0.36
|
||||
Kd 0.00 0.15 0.36
|
||||
Ks 0.00 0.15 0.36
|
||||
d 1.00
|
||||
endmtl
|
||||
249481
frontend/public/3dmimi/3D_PCB_V3-mini.obj
Normal file
265
frontend/public/3dmodo/xiaoliang1.mtl
Normal file
@@ -0,0 +1,265 @@
|
||||
# Designed by EasyEDA Pro
|
||||
newmtl mtl1
|
||||
Ka 0.59 0.46 0.00
|
||||
Kd 0.59 0.46 0.00
|
||||
Ks 0.29 0.23 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl2
|
||||
Ka 0.85 0.85 0.85
|
||||
Kd 0.85 0.85 0.85
|
||||
Ks 0.43 0.43 0.43
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl3
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 1.00 1.00 1.00
|
||||
Ks 0.88 0.88 0.88
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl4
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 1.00 1.00 1.00
|
||||
Ks 0.50 0.50 0.50
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl5
|
||||
Ka 1.00 0.89 0.81
|
||||
Kd 1.00 0.89 0.81
|
||||
Ks 0.50 0.44 0.40
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl6
|
||||
Ka 0.00 0.00 0.00
|
||||
Kd 0.00 0.00 0.00
|
||||
Ks 0.00 0.00 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl7
|
||||
Ka 0.22 0.22 0.22
|
||||
Kd 0.22 0.22 0.22
|
||||
Ks 0.11 0.11 0.11
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl8
|
||||
Ka 0.85 0.85 0.85
|
||||
Kd 0.85 0.85 0.85
|
||||
Ks 0.42 0.42 0.42
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl9
|
||||
Ka 0.75 0.75 0.75
|
||||
Kd 0.75 0.75 0.75
|
||||
Ks 0.38 0.38 0.38
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl10
|
||||
Ka 0.25 0.25 0.25
|
||||
Kd 0.25 0.25 0.25
|
||||
Ks 0.13 0.13 0.13
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl11
|
||||
Ka 0.50 0.50 0.50
|
||||
Kd 0.35 0.35 0.35
|
||||
Ks 0.02 0.02 0.02
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl12
|
||||
Ka 0.30 0.30 0.30
|
||||
Kd 0.21 0.21 0.21
|
||||
Ks 0.02 0.02 0.02
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl13
|
||||
Ka 0.67 0.67 0.67
|
||||
Kd 0.67 0.67 0.67
|
||||
Ks 0.47 0.47 0.47
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl14
|
||||
Ka 0.65 0.65 0.65
|
||||
Kd 0.65 0.65 0.65
|
||||
Ks 0.32 0.32 0.32
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl15
|
||||
Ka 0.85 0.85 0.85
|
||||
Kd 0.85 0.85 0.85
|
||||
Ks 0.68 0.68 0.68
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl16
|
||||
Ka 0.84 0.82 0.75
|
||||
Kd 0.84 0.82 0.75
|
||||
Ks 0.25 0.24 0.23
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl17
|
||||
Ka 0.25 0.25 0.25
|
||||
Kd 0.18 0.18 0.18
|
||||
Ks 0.01 0.01 0.01
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl18
|
||||
Ka 0.77 0.77 0.77
|
||||
Kd 0.77 0.77 0.77
|
||||
Ks 0.62 0.62 0.62
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl19
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 0.80 0.80 0.80
|
||||
Ks 1.00 1.00 1.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl20
|
||||
Ka 0.25 0.25 0.25
|
||||
Kd 0.20 0.20 0.20
|
||||
Ks 0.25 0.25 0.25
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl21
|
||||
Ka 0.75 0.75 0.75
|
||||
Kd 0.60 0.60 0.60
|
||||
Ks 0.75 0.75 0.75
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl22
|
||||
Ka 0.50 0.50 0.50
|
||||
Kd 0.40 0.40 0.40
|
||||
Ks 0.50 0.50 0.50
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl23
|
||||
Ka 0.25 0.25 0.25
|
||||
Kd 0.25 0.25 0.25
|
||||
Ks 0.07 0.07 0.07
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl24
|
||||
Ka 0.64 0.62 0.60
|
||||
Kd 0.45 0.43 0.42
|
||||
Ks 0.03 0.03 0.03
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl25
|
||||
Ka 0.54 0.35 0.34
|
||||
Kd 0.54 0.35 0.34
|
||||
Ks 0.38 0.24 0.24
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl26
|
||||
Ka 0.89 0.89 0.89
|
||||
Kd 0.63 0.63 0.63
|
||||
Ks 0.04 0.04 0.04
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl27
|
||||
Ka 0.83 0.67 0.13
|
||||
Kd 0.83 0.67 0.13
|
||||
Ks 0.83 0.67 0.13
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl28
|
||||
Ka 1.00 1.00 0.00
|
||||
Kd 1.00 1.00 0.00
|
||||
Ks 0.50 0.50 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl29
|
||||
Ka 1.00 0.65 0.00
|
||||
Kd 1.00 0.65 0.00
|
||||
Ks 0.50 0.33 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl30
|
||||
Ka 0.50 0.50 0.50
|
||||
Kd 0.50 0.50 0.50
|
||||
Ks 0.25 0.25 0.25
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl31
|
||||
Ka 0.79 0.82 0.93
|
||||
Kd 0.79 0.82 0.93
|
||||
Ks 0.40 0.41 0.47
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl32
|
||||
Ka 0.41 0.41 0.41
|
||||
Kd 0.41 0.41 0.41
|
||||
Ks 0.20 0.20 0.20
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl33
|
||||
Ka 0.00 0.50 0.00
|
||||
Kd 0.00 0.50 0.00
|
||||
Ks 0.00 0.25 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl34
|
||||
Ka 0.65 0.62 0.59
|
||||
Kd 0.65 0.62 0.59
|
||||
Ks 0.45 0.44 0.41
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl35
|
||||
Ka 1.00 1.00 0.50
|
||||
Kd 1.00 1.00 0.50
|
||||
Ks 0.50 0.50 0.25
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl36
|
||||
Ka 0.50 0.50 0.00
|
||||
Kd 0.50 0.50 0.00
|
||||
Ks 0.25 0.25 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl37
|
||||
Ka 1.00 0.94 0.85
|
||||
Kd 1.00 0.94 0.85
|
||||
Ks 0.30 0.28 0.25
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl38
|
||||
Ka 0.44 0.44 0.44
|
||||
Kd 0.44 0.44 0.44
|
||||
Ks 0.22 0.22 0.22
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl39
|
||||
Ka 0.83 0.83 0.82
|
||||
Kd 0.83 0.83 0.82
|
||||
Ks 0.67 0.67 0.65
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl40
|
||||
Ka 0.50 0.25 0.00
|
||||
Kd 0.50 0.25 0.00
|
||||
Ks 0.44 0.22 0.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl41
|
||||
Ka 1.00 1.00 1.00
|
||||
Kd 1.00 1.00 1.00
|
||||
Ks 1.00 1.00 1.00
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl42
|
||||
Ka 0.00 0.33 0.65
|
||||
Kd 0.00 0.33 0.65
|
||||
Ks 0.00 0.33 0.65
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl43
|
||||
Ka 0.62 0.62 0.36
|
||||
Kd 0.62 0.62 0.36
|
||||
Ks 0.62 0.62 0.36
|
||||
d 1.00
|
||||
endmtl
|
||||
newmtl mtl44
|
||||
Ka 0.00 0.15 0.36
|
||||
Kd 0.00 0.15 0.36
|
||||
Ks 0.00 0.15 0.36
|
||||
d 1.00
|
||||
endmtl
|
||||
464933
frontend/public/3dmodo/xiaoliang1.obj
Normal file
BIN
frontend/public/big_logo.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
106
frontend/public/gXEu5E01.svg
Normal file
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="300.000000pt" height="198.000000pt" viewBox="0 0 300.000000 198.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.10, written by Peter Selinger 2001-2011
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,198.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M919 1902 c-55 -54 -80 -88 -88 -116 -6 -22 -8 -42 -6 -45 3 -2 21
|
||||
14 41 37 37 44 143 112 173 112 10 0 23 -10 29 -22 16 -32 55 -179 49 -185 -3
|
||||
-2 -24 8 -46 23 -39 25 -43 26 -85 14 -41 -11 -93 -46 -83 -56 2 -2 25 5 51
|
||||
17 61 26 90 23 94 -13 4 -31 4 -31 -87 -191 -59 -106 -68 -117 -89 -112 -33 8
|
||||
-44 -2 -30 -28 6 -12 19 -51 28 -87 14 -53 22 -67 43 -74 39 -14 56 -1 88 71
|
||||
16 38 54 118 84 178 29 61 56 120 60 133 3 12 10 22 14 22 4 0 29 -42 56 -92
|
||||
27 -51 61 -112 77 -136 104 -153 300 -257 387 -206 37 22 38 33 7 67 -28 31
|
||||
-67 51 -125 67 -51 13 -139 67 -190 114 -18 17 -60 66 -92 109 -66 88 -92 158
|
||||
-87 234 3 43 5 48 28 48 38 0 30 46 -17 100 -31 35 -52 47 -118 69 -44 14 -82
|
||||
26 -85 26 -3 0 -39 -35 -81 -78z"/>
|
||||
<path d="M2575 1910 c-3 -6 5 -14 18 -19 32 -12 40 -26 33 -54 -7 -30 -59 -59
|
||||
-98 -55 -42 4 -46 -15 -9 -48 17 -15 31 -30 31 -34 0 -3 -25 -17 -56 -29 -54
|
||||
-23 -92 -60 -80 -80 5 -7 15 -6 31 1 42 19 70 -1 72 -53 1 -24 -3 -50 -10 -58
|
||||
-10 -12 -7 -13 21 -9 59 9 28 -16 -69 -54 -60 -24 -111 -37 -141 -38 -39 0
|
||||
-46 -3 -41 -16 3 -9 19 -22 36 -29 27 -11 38 -9 119 26 93 41 118 47 118 27 0
|
||||
-26 -52 -97 -87 -118 -34 -20 -36 -23 -18 -30 39 -14 113 22 132 63 30 64 43
|
||||
99 43 111 0 24 135 37 156 16 7 -7 51 -10 110 -8 92 3 99 4 102 24 3 18 -8 26
|
||||
-60 48 l-64 27 -97 -15 c-53 -9 -99 -16 -103 -16 -3 0 -4 10 -2 23 2 17 15 26
|
||||
53 38 72 24 70 38 -9 63 -18 6 -17 9 9 36 18 19 26 36 22 49 -6 24 16 46 61
|
||||
62 18 6 32 14 32 19 0 10 -57 82 -77 98 -9 6 -33 12 -52 12 -20 1 -47 7 -61
|
||||
15 -30 17 -56 19 -65 5z m139 -98 c-31 -75 -50 -71 -28 6 13 43 18 51 30 41
|
||||
11 -9 11 -17 -2 -47z m-94 -157 c0 -7 -12 -23 -26 -34 -24 -19 -27 -19 -60 -4
|
||||
-19 9 -34 19 -34 23 0 3 19 15 43 27 44 21 77 17 77 -12z"/>
|
||||
<path d="M579 1723 c-1 -4 0 -20 1 -35 1 -20 -3 -28 -15 -28 -10 0 -26 -11
|
||||
-37 -25 -11 -14 -27 -25 -36 -25 -10 0 -22 -10 -27 -22 -7 -16 -18 -22 -33
|
||||
-21 -25 3 -30 -14 -7 -23 22 -9 19 -71 -7 -107 -12 -18 -15 -26 -8 -19 19 16
|
||||
30 15 30 -4 0 -15 -37 -53 -53 -54 -16 0 -47 93 -47 144 0 43 5 58 26 81 14
|
||||
15 24 28 22 29 -2 0 -23 11 -48 24 -25 12 -48 22 -52 22 -12 0 -9 -34 6 -71 7
|
||||
-19 19 -67 26 -108 7 -41 19 -89 26 -107 20 -47 18 -52 -21 -71 -28 -14 -35
|
||||
-22 -35 -46 l0 -30 61 7 c47 4 82 0 159 -19 115 -29 183 -31 242 -8 59 22 71
|
||||
32 57 49 -8 10 -31 11 -93 7 l-83 -6 -6 39 c-4 22 -7 67 -7 100 l0 61 43 16
|
||||
c23 8 55 18 70 21 16 4 27 12 25 18 -7 19 -70 37 -110 31 -31 -4 -38 -2 -38
|
||||
11 0 9 9 21 20 28 25 16 26 44 0 58 -10 6 -22 26 -26 45 -6 32 -23 57 -25 38z
|
||||
m1 -211 c0 -34 -4 -40 -35 -55 -30 -14 -35 -15 -35 -2 0 20 -10 19 -26 -2 -12
|
||||
-17 -13 -16 -14 4 0 12 8 24 18 27 10 4 31 20 47 36 16 17 32 30 37 30 4 0 8
|
||||
-17 8 -38z m0 -124 c0 -52 -13 -65 -44 -44 -37 23 -32 12 14 -38 23 -24 37
|
||||
-47 33 -49 -5 -3 -40 -2 -78 3 -39 5 -78 9 -87 10 -12 0 -18 8 -18 24 0 31 46
|
||||
69 72 60 14 -4 21 0 25 15 5 21 46 50 71 51 7 0 12 -13 12 -32z"/>
|
||||
<path d="M487 1643 c-4 -3 -7 -11 -7 -17 0 -6 5 -5 12 2 6 6 9 14 7 17 -3 3
|
||||
-9 2 -12 -2z"/>
|
||||
<path d="M1928 1644 c-26 -8 -22 -24 5 -24 74 0 143 -44 131 -83 -14 -43 -118
|
||||
-190 -189 -267 -40 -42 -85 -96 -100 -119 l-28 -41 19 -38 c26 -51 64 -92 86
|
||||
-92 9 0 31 19 48 43 17 23 54 73 81 111 108 147 189 299 189 352 0 48 -48 107
|
||||
-110 133 -56 24 -104 33 -132 25z"/>
|
||||
<path d="M2660 1395 c0 -2 34 -27 75 -55 41 -28 75 -56 75 -63 0 -7 18 -30 39
|
||||
-51 48 -46 75 -46 79 3 4 39 -26 76 -93 116 -47 27 -175 64 -175 50z"/>
|
||||
<path d="M2130 1227 c0 -8 12 -22 26 -32 14 -9 75 -64 135 -121 l110 -105 47
|
||||
3 c42 3 47 6 50 29 3 22 -7 35 -55 75 -32 26 -83 58 -113 71 -30 13 -84 38
|
||||
-119 56 -85 42 -81 41 -81 24z"/>
|
||||
<path d="M522 1073 c-47 -50 -85 -100 -152 -197 -134 -194 -192 -276 -199
|
||||
-281 -5 -3 -19 -26 -31 -51 -13 -25 -47 -78 -77 -117 -29 -39 -56 -81 -59 -93
|
||||
-7 -28 22 -126 47 -158 11 -15 31 -26 43 -26 19 0 32 19 80 115 32 63 75 134
|
||||
96 158 22 23 44 54 50 68 11 24 17 27 93 32 45 4 110 9 144 13 l62 7 6 -39 c4
|
||||
-22 9 -47 12 -57 3 -13 -2 -20 -16 -24 -25 -6 -25 -5 -7 -63 36 -115 96 -178
|
||||
112 -116 18 73 -35 570 -75 703 -5 18 -7 57 -4 86 12 114 -40 130 -125 40z
|
||||
m72 -160 c3 -21 8 -69 11 -108 3 -38 8 -91 11 -116 l5 -46 -93 -5 c-51 -3
|
||||
-101 -9 -112 -13 -16 -7 -17 -5 -11 16 4 13 23 49 43 79 19 30 40 68 46 85 14
|
||||
38 78 145 87 145 4 0 10 -17 13 -37z"/>
|
||||
<path d="M2788 994 c-4 -3 -1 -13 7 -21 8 -8 15 -23 15 -32 0 -24 19 -39 58
|
||||
-47 28 -6 32 -4 32 15 0 41 -89 109 -112 85z"/>
|
||||
<path d="M1245 974 c-33 -8 -84 -13 -113 -11 -40 3 -55 0 -59 -10 -10 -26 32
|
||||
-55 84 -60 l49 -5 -12 -47 c-18 -68 -112 -318 -144 -382 -40 -78 -46 -82 -137
|
||||
-78 -77 4 -77 4 -87 -24 -7 -19 -7 -33 1 -45 9 -15 34 -17 242 -16 205 1 234
|
||||
4 251 19 27 24 12 48 -42 68 -54 20 -83 23 -131 13 -21 -5 -40 -6 -43 -3 -7 7
|
||||
29 89 56 126 11 16 20 38 20 49 0 22 35 105 82 197 18 35 31 76 32 100 l1 40
|
||||
75 3 c104 4 116 27 33 63 -51 22 -74 22 -158 3z"/>
|
||||
<path d="M2600 984 c0 -8 36 -133 66 -226 39 -124 38 -126 -60 -181 -47 -26
|
||||
-89 -47 -94 -47 -6 0 -24 -16 -40 -36 -24 -29 -30 -32 -36 -19 -7 20 -6 22 56
|
||||
103 61 81 88 133 88 172 0 45 -38 96 -91 121 -56 26 -81 18 -49 -16 21 -22 21
|
||||
-24 5 -56 -23 -44 -74 -108 -154 -192 l-65 -67 17 -35 c9 -20 17 -42 17 -49 0
|
||||
-35 63 -59 88 -34 18 18 21 7 11 -42 -8 -40 -6 -51 18 -100 16 -30 36 -56 46
|
||||
-58 15 -3 17 7 17 106 0 128 8 143 81 161 25 6 77 24 115 40 49 21 71 26 75
|
||||
18 3 -7 16 -50 28 -97 12 -47 33 -120 47 -163 13 -43 24 -86 24 -96 0 -30 81
|
||||
-168 107 -182 31 -17 54 -4 65 37 11 40 2 423 -10 454 -10 23 -11 23 -11 4 -1
|
||||
-26 -47 -167 -69 -209 l-15 -30 -22 65 c-11 36 -26 74 -32 85 -15 26 -53 144
|
||||
-53 165 0 9 12 23 28 31 15 8 42 24 61 37 19 12 49 22 67 22 18 0 36 5 40 12
|
||||
13 20 -34 46 -108 58 -40 7 -91 20 -113 29 -37 17 -42 23 -64 92 -31 96 -48
|
||||
129 -66 129 -8 0 -15 -3 -15 -6z m-176 -266 c-35 -50 -155 -180 -161 -174 -3
|
||||
2 37 54 89 114 94 112 125 137 72 60z m346 22 c21 -11 41 -24 45 -29 5 -9 -43
|
||||
-41 -60 -41 -7 0 -35 64 -35 80 0 14 5 13 50 -10z"/>
|
||||
<path d="M1946 878 c-3 -7 -4 -20 -5 -28 0 -8 -3 -56 -7 -106 l-7 -92 -79 -31
|
||||
c-44 -17 -86 -31 -94 -31 -19 0 -18 -16 2 -24 29 -11 79 -6 123 14 24 11 45
|
||||
20 47 20 2 0 4 -30 4 -66 l0 -66 -28 5 c-16 3 -32 7 -38 8 -49 9 -104 -21
|
||||
-104 -58 0 -15 2 -16 10 -3 12 19 104 20 138 2 18 -11 22 -21 22 -61 0 -27 -3
|
||||
-63 -6 -80 -7 -32 -8 -32 -48 -25 -45 7 -77 21 -88 38 -6 8 -8 8 -8 0 0 -18
|
||||
163 -174 182 -174 32 0 38 45 38 285 l0 233 46 16 c26 9 50 13 55 11 4 -3 11
|
||||
-1 14 5 9 15 -13 40 -35 40 -32 0 -80 49 -80 81 0 59 -42 126 -54 87z"/>
|
||||
<path d="M1531 674 c-11 -14 -26 -46 -34 -72 -40 -130 -39 -125 -22 -167 34
|
||||
-81 68 -95 115 -45 l28 30 16 -25 c21 -32 50 -32 64 -1 31 68 21 237 -16 279
|
||||
-18 20 -53 22 -71 5 -10 -11 -14 -10 -22 5 -14 24 -34 21 -58 -9z m119 -64 c0
|
||||
-31 -4 -40 -17 -40 -10 0 -30 -7 -45 -14 -36 -19 -37 -2 -3 44 46 62 65 65 65
|
||||
10z m-11 -103 c-23 -45 -30 -49 -50 -31 -26 24 -24 33 13 49 40 16 52 11 37
|
||||
-18z m-61 -68 c-13 -8 -28 7 -28 30 0 11 5 10 20 -4 15 -14 17 -20 8 -26z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
BIN
frontend/public/liangji_black.png
Normal file
|
After Width: | Height: | Size: 281 KiB |
35
frontend/public/liangji_logo.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 989.55 925.64">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
font-family: Krungthep, Krungthep;
|
||||
font-size: 92.87px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #020202;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_图层_1-2" data-name="图层_1">
|
||||
<g>
|
||||
<path class="cls-2" d="M412.28,0c21.28,5.72,40.43,15.59,58.14,28.66,7.12,5.25,12.47,11.82,17.59,18.76,1.92,2.6,4.75,5.22,3.21,8.74-1.62,3.73-5.57,2.84-8.73,2.95-16.39.58-32.74-.2-49.19,2.09-42.55,5.92-78.84,24.31-111.03,52.05-48.48,41.78-82.1,94.79-111.85,150.21-30.11,56.1-56.75,113.89-72.13,176.2-11.14,45.15-19.93,90.7-19.44,137.28.31,29.14,4.18,58.21,20.59,83.77,1.77,2.75,3.54,5.55,5.67,8.02,10.44,12.13,10.23,13.09-5.05,19.39-53.28,21.97-109.47-4.38-130.35-60.79-4.59-12.4-5.9-25.5-9.73-38.02v-27.12c2.91-18.86,3.06-37.98,4.31-56.94,2.44-36.98,12.09-72.25,22.29-107.44,19.91-68.68,49.59-132.98,88.75-192.85,18.68-28.57,39.12-55.93,62.76-80.53,35.21-36.64,68.51-75.39,113.61-101.27,23.59-13.53,48.25-21.17,75.24-21.91,2.01-.06,4.13.36,5.82-1.23h39.5Z"/>
|
||||
<path class="cls-2" d="M792.19,131.3c-16.86,4.02-33.77,6.89-49.92,12.11-60.19,19.46-111.99,51.88-154.93,98.96-55.08,60.4-107.14,123.35-158.47,186.84-48.14,59.56-99.58,115.86-154.96,168.6-22.57,21.49-45.48,42.8-71.8,59.89-22.13,14.36-41.19,9.39-52.67-14.17-11.63-23.86-13.51-49.64-13.52-75.67,0-4.89,2.35-8.34,6.05-11.5,47.42-40.54,94.77-81.15,142.07-121.83,43.56-37.47,87.81-74.19,130.38-112.76,75.92-68.77,157.11-129.65,250.52-172.89,36.83-17.05,76.31-22.18,116.48-22.94,3.81-.07,8.38-.48,10.77,5.37Z"/>
|
||||
<path class="cls-2" d="M795.46,490.45c-20.17-4.55-38.34-12.09-54.96-22.98-44.94-29.47-80.24-68.78-112.48-111.11-8.45-11.1-16-22.91-24.58-33.91-5.85-7.5-4.36-12.69,2.13-18.88,15.17-14.45,30.01-29.27,44.46-44.45,6.64-6.98,11.38-6.99,18.18-.06,24.18,24.64,49.97,47.41,78.35,67.31,34.67,24.31,72.81,39.36,114.51,45.94,8.93,1.41,17.75,3.48,26.67,4.93,7.45,1.21,10.05,4.16,6.34,11.67-20.49,41.45-48.2,76.2-90.04,98.03-2.86,1.49-5.98,2.47-8.57,3.52Z"/>
|
||||
<path class="cls-2" d="M874.18,155.7c17.01,14.01,30.76,29.06,43.52,44.96,31.34,39.05,54.92,81.89,61.08,132.44,5.37,44.12-2.85,86.74-30.05,121.33-25.28,32.16-62,47.85-104.39,45.61-4.25-.22-10.55,0-11.8-4.7-1.41-5.29,5.19-6.47,8.54-9.01,46.66-35.45,80.06-80.91,87.6-139.08,8.67-66.91-6.32-129.76-51.65-182.74-1.54-1.8-3.86-3.24-2.84-8.8Z"/>
|
||||
<path class="cls-2" d="M393.2,88.88c29.32-14.72,57.46-19.83,87.21-16.33,3.67.43,7.31,1.12,10.99,1.51,19.26,2.03,35.26,8.92,46,26.49,6.81,11.15,15.5,21.23,23.97,31.26,5.93,7.03,3.97,11.44-2.86,15.78-13.88,8.82-27.81,17.57-41.36,26.87-6.79,4.66-10.95,2.94-15.26-3.19-19.76-28.08-45.74-49.27-75.28-66.2-9.89-5.67-20.53-10.01-33.4-16.19Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-2" d="M642.58,733.79c.17,2.58.53,4.27.34,5.88-1.29,10.63-1.84,11.14-12.88,11.16-28.21.04-56.41,0-84.62.06-3.05,0-6.28.09-9.09,1.09-1.33.48-2.39,3.16-2.52,4.92-.07.87,2.12,2.52,3.49,2.77,2.67.5,5.48.32,8.22.32,28.89.02,57.79.01,86.68.02,2.41,0,4.82.11,7.22.04,5.04-.15,6.34,2.43,6.2,7.12-.43,15.28-2,16.99-17.13,16.99-88.4,0-176.81-.05-265.21.05-16.7.02-14.53-1.71-12.47-14.02,1.68-10.04,1.54-10.14,12.36-10.16,30.27-.04,60.54,0,90.81-.04,3.08,0,6.23.02,9.22-.61,1.25-.27,3.14-2.35,2.99-3.35-.24-1.64-1.63-4.01-3.02-4.41-2.89-.83-6.1-.73-9.18-.73-25.45-.04-50.91-.03-76.36-.03-2.41,0-4.82.05-7.22-.02-7.95-.25-8.85-.92-8.95-6.56-.15-8.44,1.77-10.89,9.63-10.93,28.21-.12,56.41-.05,84.62-.07,3.1,0,6.19.02,9.28-.13,2.92-.15,5.62-.87,5.47-4.61-.15-3.75-3.08-4.1-5.87-4.28-2.74-.18-5.5-.12-8.25-.13-23.05,0-46.09,0-69.14,0-2.41,0-4.82.07-7.22-.03-7.8-.33-8.69-1.35-7.79-9.4.46-4.08,1.25-8.12,1.82-12.19,1.85-13.22,3.54-26.47,5.56-39.67,1.48-9.68,1.67-9.77,11.82-9.8,24.76-.07,49.53-.02,74.29-.02,55.72,0,111.45,0,167.17,0,2.75,0,5.51.13,8.25.02,4.93-.19,6.92,2.06,6.23,6.91-2.74,19.32-5.51,38.64-8.1,57.98-.56,4.21-2.71,6.05-6.76,6.09-4.81.04-9.63.1-14.45.1-22.7.01-45.41,0-68.11.02-2.75,0-5.66-.38-8.19.4-1.9.58-3.3,2.75-4.93,4.2,1.56,1.45,2.89,3.61,4.72,4.16,2.54.76,5.45.37,8.19.37,27.17.02,54.35,0,81.52.03,3.68,0,7.35.31,11.35.5ZM455.08,670.86c-6.47,0-12.98-.37-19.4.21-2.05.19-5.2,2.68-5.5,4.49-.61,3.7,3.08,3.81,5.74,3.84,10.56.13,21.13.1,31.69.08,2.04,0,4.39.35,6.03-.52,1.78-.94,2.89-3.12,4.3-4.75-1.49-1.12-2.92-3.11-4.47-3.2-6.11-.37-12.25-.15-18.39-.16ZM568.87,679.4v.1c5.11,0,10.22.09,15.33-.04,2.01-.05,4.37-.05,5.89-1.08,1.37-.93,2.31-3.18,2.45-4.93.06-.77-2.3-2.49-3.58-2.51-12.94-.17-25.89-.23-38.83.12-1.75.05-4.55,2.23-4.91,3.86-.82,3.65,2.44,4.37,5.25,4.44,6.13.15,12.26.05,18.4.05ZM448.88,706.09s0,.06,0,.09c5.83,0,11.66.09,17.49-.04,3.45-.08,7.64-.22,7.54-4.87-.09-4.28-4.23-3.76-7.17-3.79-10.63-.12-21.26-.08-31.89-.03-1.7,0-3.72-.16-5.01.66-1.52.96-3.2,2.87-3.28,4.45-.06,1.11,2.39,3.27,3.82,3.36,6.15.38,12.33.16,18.5.16ZM564.82,706.13c0-.07,0-.13,0-.2,6.52,0,13.07.31,19.56-.21,1.66-.13,3.1-2.91,4.64-4.47-1.66-1.25-3.27-3.5-4.99-3.58-8.23-.4-16.49-.27-24.74-.23-4.46.02-8.99-.2-13.35.53-1.79.3-3.19,2.85-4.77,4.37,1.7,1.27,3.32,3.49,5.1,3.62,6.15.45,12.36.17,18.54.17Z"/>
|
||||
<path class="cls-2" d="M868.37,736.34c.93-2.22,1.01-3.06,1.46-3.4,12.08-9.08,14.69-22.06,16.41-36.11,3.71-30.24,8.49-60.36,12.83-90.52.2-1.36.44-2.7.64-4.06.49-3.35.06-6.38-3.88-7.18-3.4-.69-7.06-1.04-9.18,2.61-1.19,2.05-1.99,4.37-2.64,6.66-11.58,40.52-23.16,81.03-34.64,121.58-2.94,10.37-2.8,10.46-14.05,10.52-10.91.06-21.82.01-34.19.01,3.56-12.77,6.6-23.89,9.76-34.99,9.17-32.29,18.43-64.56,27.56-96.87,2.22-7.86.78-9.72-7.81-9.94-9.28-.24-18.57-.03-27.86-.1-3.38-.02-6.75-.28-10.77-.47-1.09-9.14.84-17.37,2.71-25.55,1.05-4.59,5.14-3.82,8.47-3.83,16.51-.07,33.02-.04,49.54-.04,2.41,0,4.82.03,7.22-.01,10.21-.18,14.44-7.01,9.93-16.18-1.15-2.34-2.73-4.48-4.59-7.48,2.77-.7,4.63-1.57,6.5-1.58,16.17-.12,32.34-.22,48.5-.02,7.51.09,8.2,1.23,7.24,8.67q-2.15,16.59,14.3,16.61c12.04,0,24.08-.03,36.12.03,3.36.02,6.73.31,11.61.56-1.25,8.79-2.38,16.77-3.51,24.75-.58,4.08-3.6,4.52-6.87,4.53-7.57.03-15.14-.09-22.7.1-7.82.19-8.56.8-9.73,8.79-4.84,33.21-9.54,66.43-14.35,99.65-1.83,12.66-6.05,24.01-18.32,30.38-2.67,1.39-5.76,2.69-8.69,2.77-11.99.31-23.98.13-37.02.13Z"/>
|
||||
<path class="cls-2" d="M971.24,753.75c-1.38,9.62-2.64,17.36-3.55,25.14-.54,4.61-2.77,6.73-7.38,6.6-2.75-.08-5.5.03-8.25.03-48.49,0-96.98-.46-145.45.24-17.85.26-34.82-2.54-51.64-7.68-1.97-.6-3.94-1.22-5.92-1.79-5.09-1.46-9.24-.84-12.55,4.08-1.51,2.24-4.61,4.76-7.05,4.83-15.41.44-30.83.22-46.48.22-1.46-5.71,1.81-9.4,4.11-12.69,8.56-12.25,11.46-26.01,13.27-40.48,3.4-27.17,7.57-54.24,11.39-81.36.33-2.37.54-4.76.72-7.15.35-4.67-.75-8.27-6.45-8.21-5.25.06-6.35-3.05-5.79-7.44.31-2.37.68-4.74,1.06-7.11q2.55-15.9,19.17-15.91c13.76,0,27.51-.1,41.26.05,9.64.1,10.77,1.29,9.45,11-3.59,26.45-7.42,52.88-11.17,79.31-1.54,10.85-3.22,21.67-4.63,32.54-1.32,10.15-.37,11.46,9.82,14.39,10.24,2.95,20.42,6.14,30.79,8.52,6.96,1.59,14.2,2.7,21.32,2.74,46.77.24,93.53.12,140.3.12,4.06,0,8.13,0,13.66,0Z"/>
|
||||
<path class="cls-2" d="M386.62,616.93c1.7-14.64,3.11-27.81,4.81-40.94,1.05-8.13,2.62-16.19,3.84-24.3.76-5.11,3.03-8.23,8.76-7.58,1.36.15,2.75,0,4.12,0,81.07,0,162.13,0,243.2.02,3.67,0,7.33.35,12.14.6-1.25,9.12-2.35,17.17-3.45,25.23-1.8,13.21-3.71,26.41-5.33,39.64-.67,5.47-3.29,7.89-8.82,7.35-2.04-.2-4.12-.02-6.18-.02-80.04,0-160.07,0-240.11,0-4.01,0-8.02,0-12.97,0ZM523.06,590.31c-22.98,0-45.96,0-68.94.01-2.74,0-5.59-.25-8.18.44-1.6.42-3.75,2.3-3.88,3.69-.11,1.29,1.88,3.34,3.41,4.03,1.76.79,4.03.56,6.08.56,47.68.02,95.36.02,143.03,0,2.05,0,4.33.26,6.08-.53,1.52-.69,3.45-2.76,3.36-4.09-.1-1.36-2.26-3.25-3.84-3.67-2.59-.68-5.44-.42-8.18-.42-22.98-.02-45.96-.01-68.94-.01ZM526.45,570.84c24.39,0,48.78.04,73.17-.05,3.23-.01,8.18.98,8.19-3.73,0-4.9-5.06-3.51-8.17-3.54-16.13-.14-32.27-.03-48.4-.03-31.26,0-62.52-.04-93.78-.04-2.74,0-5.56-.1-8.19.51-1.31.3-3.05,2.05-3.13,3.25-.07,1.02,1.79,2.87,3.07,3.15,2.64.58,5.45.46,8.19.46,23.02.03,46.03.01,69.05,0Z"/>
|
||||
<path class="cls-2" d="M664.1,626.08c.35,2.15.7,3.13.65,4.09-.72,13.47-.79,13.54-14.36,13.56-68.72.09-137.44.18-206.16.22-21.6.01-43.19-.1-64.79-.24-9.57-.07-12.19-3.9-8.35-12.73.89-2.03,3.57-3.94,5.81-4.58,2.87-.81,6.13-.31,9.22-.32,59.78,0,119.56,0,179.34,0,29.21,0,58.41,0,87.62,0,3.68,0,7.37,0,11.02,0Z"/>
|
||||
<path class="cls-2" d="M951.96,661.67c0-10.27-.11-20.54.03-30.8.12-8.74.54-9.1,9.52-9.28,5.49-.11,10.99.02,16.49.09,3.73.05,5.71,1.93,5.76,5.7.03,2.74.15,5.48.06,8.21-.56,18.79-1.12,37.58-1.77,56.37-.32,9.24-.43,9.31-9.23,9.51-4.81.11-9.62-.12-14.43,0-4.9.12-6.67-2.33-6.63-6.94.1-10.95.03-21.9.03-32.86.05,0,.11,0,.16,0Z"/>
|
||||
<path class="cls-2" d="M769.69,700.98c2.74-10.45,5-19.3,7.4-28.12,4.14-15.17,8.28-30.34,12.59-45.46,2.46-8.64,3.22-9.12,12.08-9.24,6.07-.09,12.13-.02,19.5-.02-1.06,4.9-1.61,8.18-2.48,11.38-5.76,21.1-11.57,42.2-17.42,63.28-2.35,8.46-2.41,8.57-11.24,8.65-6.35.06-12.7-.28-20.42-.47Z"/>
|
||||
<path class="cls-2" d="M778.14,586.16c-8.58,0-15.33,0-22.09,0-7.56,0-15.12-.07-22.68-.02-3.38.03-6.1-.8-7.29-4.34-3.77-11.29-7.54-22.59-11.52-34.5,2.84-.76,4.7-1.68,6.58-1.7,12.03-.14,24.06-.1,36.08-.07,3.48,0,7.16,0,8.57,4.09,3.98,11.56,7.84,23.16,12.35,36.53Z"/>
|
||||
</g>
|
||||
<text class="cls-1" transform="translate(360.91 887.84)"><tspan x="0" y="0">QUANT SPEED</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
BIN
frontend/public/liangji_white.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
1
frontend/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
41
frontend/src/App.css
Normal file
@@ -0,0 +1,41 @@
|
||||
#root {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
30
frontend/src/App.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import Layout from './components/Layout';
|
||||
import Home from './pages/Home';
|
||||
import ProductDetail from './pages/ProductDetail';
|
||||
import Payment from './pages/Payment';
|
||||
import AIServices from './pages/AIServices';
|
||||
import ServiceDetail from './pages/ServiceDetail';
|
||||
import ARExperience from './pages/ARExperience';
|
||||
import 'antd/dist/reset.css';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/services" element={<AIServices />} />
|
||||
<Route path="/services/:id" element={<ServiceDetail />} />
|
||||
<Route path="/ar" element={<ARExperience />} />
|
||||
<Route path="/product/:id" element={<ProductDetail />} />
|
||||
<Route path="/payment/:orderId" element={<Payment />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
21
frontend/src/api.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000/api',
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
export const getConfigs = () => api.get('/configs/');
|
||||
export const createOrder = (data) => api.post('/orders/', data);
|
||||
export const getOrder = (id) => api.get(`/orders/${id}/`);
|
||||
export const initiatePayment = (orderId) => api.post(`/orders/${orderId}/initiate_payment/`);
|
||||
export const confirmPayment = (orderId) => api.post(`/orders/${orderId}/confirm_payment/`);
|
||||
|
||||
export const getServices = () => api.get('/services/');
|
||||
export const getServiceDetail = (id) => api.get(`/services/${id}/`);
|
||||
export const getARServices = () => api.get('/ar/');
|
||||
|
||||
export default api;
|
||||
1
frontend/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
161
frontend/src/components/Layout.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Layout as AntLayout, Menu, ConfigProvider, theme, Drawer, Button } from 'antd';
|
||||
import { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import ParticleBackground from './ParticleBackground';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
const { Header, Content, Footer } = AntLayout;
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: '/',
|
||||
icon: <RobotOutlined />,
|
||||
label: 'AI 硬件',
|
||||
},
|
||||
{
|
||||
key: '/services',
|
||||
icon: <AppstoreOutlined />,
|
||||
label: 'AI 服务',
|
||||
},
|
||||
{
|
||||
key: '/ar',
|
||||
icon: <EyeOutlined />,
|
||||
label: 'AR 体验',
|
||||
},
|
||||
];
|
||||
|
||||
const handleMenuClick = (key) => {
|
||||
navigate(key);
|
||||
setMobileMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: theme.darkAlgorithm,
|
||||
token: {
|
||||
colorPrimary: '#00b96b',
|
||||
colorBgContainer: 'transparent',
|
||||
colorBgLayout: 'transparent',
|
||||
fontFamily: "'Orbitron', sans-serif",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ParticleBackground />
|
||||
<AntLayout style={{ minHeight: '100vh', background: 'transparent' }}>
|
||||
<Header
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 1000,
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
maxWidth: '1440px',
|
||||
padding: '0 20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
height: '100%'
|
||||
}}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
style={{
|
||||
color: '#fff',
|
||||
fontSize: '20px',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
<img src="/liangji_white.png" alt="Quant-Speed Logo" style={{ height: '32px' }} />
|
||||
</motion.div>
|
||||
|
||||
{/* Desktop Menu */}
|
||||
<div className="desktop-menu" style={{ display: 'none' }}>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="horizontal"
|
||||
selectedKeys={[location.pathname]}
|
||||
items={items}
|
||||
onClick={(e) => handleMenuClick(e.key)}
|
||||
style={{ background: 'transparent', borderBottom: 'none', minWidth: 300 }}
|
||||
/>
|
||||
</div>
|
||||
<style>{`
|
||||
@media (min-width: 768px) {
|
||||
.desktop-menu { display: block !important; }
|
||||
.mobile-menu-btn { display: none !important; }
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<Button
|
||||
className="mobile-menu-btn"
|
||||
type="text"
|
||||
icon={<MenuOutlined style={{ color: '#fff', fontSize: 20 }} />}
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
{/* Mobile Drawer Menu */}
|
||||
<Drawer
|
||||
title={<span style={{ color: '#00b96b' }}>导航菜单</span>}
|
||||
placement="right"
|
||||
onClose={() => setMobileMenuOpen(false)}
|
||||
open={mobileMenuOpen}
|
||||
styles={{ body: { padding: 0, background: '#111' }, header: { background: '#111', borderBottom: '1px solid #333' }, wrapper: { width: 250 } }}
|
||||
>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="vertical"
|
||||
selectedKeys={[location.pathname]}
|
||||
items={items}
|
||||
onClick={(e) => handleMenuClick(e.key)}
|
||||
style={{ background: 'transparent', borderRight: 'none' }}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
<Content style={{ marginTop: 64, padding: '24px', overflowX: 'hidden' }}>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={location.pathname}
|
||||
initial={{ opacity: 0, y: 20, filter: 'blur(10px)' }}
|
||||
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
|
||||
exit={{ opacity: 0, y: -20, filter: 'blur(10px)' }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Content>
|
||||
|
||||
<Footer style={{ textAlign: 'center', background: 'rgba(0,0,0,0.5)', color: '#666', backdropFilter: 'blur(5px)' }}>
|
||||
Quant-Speed AI Hardware ©{new Date().getFullYear()} Created by Quant-Speed Tech
|
||||
</Footer>
|
||||
</AntLayout>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
64
frontend/src/components/ModelViewer.jsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { Canvas, useLoader } from '@react-three/fiber';
|
||||
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
|
||||
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
|
||||
import { OrbitControls, Stage, useProgress } from '@react-three/drei';
|
||||
import { Spin } from 'antd';
|
||||
|
||||
const Model = ({ objPath, mtlPath, scale = 1 }) => {
|
||||
const materials = useLoader(MTLLoader, mtlPath);
|
||||
|
||||
const obj = useLoader(OBJLoader, objPath, (loader) => {
|
||||
materials.preload();
|
||||
loader.setMaterials(materials);
|
||||
});
|
||||
|
||||
const clone = obj.clone();
|
||||
return <primitive object={clone} scale={scale} />;
|
||||
};
|
||||
|
||||
const LoadingOverlay = () => {
|
||||
const { progress, active } = useProgress();
|
||||
if (!active) return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
zIndex: 10,
|
||||
pointerEvents: 'none'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ color: '#00b96b', marginTop: 10, fontWeight: 'bold' }}>
|
||||
{progress.toFixed(0)}% Loading
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ModelViewer = ({ objPath, mtlPath, scale = 1, autoRotate = true }) => {
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
<LoadingOverlay />
|
||||
<Canvas shadows dpr={[1, 2]} camera={{ fov: 45 }} style={{ height: '100%', width: '100%' }}>
|
||||
<Suspense fallback={null}>
|
||||
<Stage environment="city" intensity={0.5}>
|
||||
<Model objPath={objPath} mtlPath={mtlPath} scale={scale} />
|
||||
</Stage>
|
||||
</Suspense>
|
||||
<OrbitControls autoRotate={autoRotate} />
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelViewer;
|
||||
92
frontend/src/components/ParticleBackground.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
const ParticleBackground = () => {
|
||||
const canvasRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext('2d');
|
||||
let animationFrameId;
|
||||
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
const particles = [];
|
||||
const particleCount = 100;
|
||||
|
||||
class Particle {
|
||||
constructor() {
|
||||
this.x = Math.random() * canvas.width;
|
||||
this.y = Math.random() * canvas.height;
|
||||
this.vx = (Math.random() - 0.5) * 0.5;
|
||||
this.vy = (Math.random() - 0.5) * 0.5;
|
||||
this.size = Math.random() * 2;
|
||||
this.color = Math.random() > 0.5 ? 'rgba(0, 185, 107, ' : 'rgba(0, 240, 255, '; // Green or Blue
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
|
||||
if (this.x < 0 || this.x > canvas.width) this.vx *= -1;
|
||||
if (this.y < 0 || this.y > canvas.height) this.vy *= -1;
|
||||
}
|
||||
|
||||
draw() {
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = this.color + Math.random() * 0.5 + ')';
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
particles.push(new Particle());
|
||||
}
|
||||
|
||||
const animate = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw connecting lines
|
||||
ctx.lineWidth = 0.5;
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
for (let j = i; j < particleCount; j++) {
|
||||
const dx = particles[i].x - particles[j].x;
|
||||
const dy = particles[i].y - particles[j].y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 100) {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = `rgba(100, 255, 218, ${1 - distance / 100})`;
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
particles.forEach(p => {
|
||||
p.update();
|
||||
p.draw();
|
||||
});
|
||||
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeCanvas);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <canvas ref={canvasRef} id="particle-canvas" />;
|
||||
};
|
||||
|
||||
export default ParticleBackground;
|
||||
59
frontend/src/index.css
Normal file
@@ -0,0 +1,59 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Orbitron', 'Roboto', sans-serif; /* 假设引入了科技感字体 */
|
||||
background-color: #050505;
|
||||
color: #fff;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 全局滚动条美化 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #000;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #00b96b;
|
||||
}
|
||||
|
||||
/* 霓虹光效工具类 */
|
||||
.neon-text-green {
|
||||
color: #00b96b;
|
||||
text-shadow: 0 0 5px rgba(0, 185, 107, 0.5), 0 0 10px rgba(0, 185, 107, 0.3);
|
||||
}
|
||||
|
||||
.neon-text-blue {
|
||||
color: #00f0ff;
|
||||
text-shadow: 0 0 5px rgba(0, 240, 255, 0.5), 0 0 10px rgba(0, 240, 255, 0.3);
|
||||
}
|
||||
|
||||
.neon-border {
|
||||
border: 1px solid rgba(0, 185, 107, 0.3);
|
||||
box-shadow: 0 0 10px rgba(0, 185, 107, 0.1), inset 0 0 10px rgba(0, 185, 107, 0.1);
|
||||
}
|
||||
|
||||
/* 玻璃拟态 */
|
||||
.glass-panel {
|
||||
background: rgba(20, 20, 20, 0.6);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
/* 粒子背景容器 */
|
||||
#particle-canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
10
frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
172
frontend/src/pages/AIServices.jsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Row, Col, Typography, Button, Spin } from 'antd';
|
||||
import { motion } from 'framer-motion';
|
||||
import { RightOutlined } from '@ant-design/icons';
|
||||
import { getServices } from '../api';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const AIServices = () => {
|
||||
const [services, setServices] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchServices = async () => {
|
||||
try {
|
||||
const response = await getServices();
|
||||
setServices(response.data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch services:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchServices();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '100px 0' }}>
|
||||
<Spin size="large" tip="Loading services..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px 0' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 60 }}>
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<Title level={1} style={{ color: '#fff', fontSize: 'clamp(2rem, 4vw, 3rem)' }}>
|
||||
AI 全栈<span style={{ color: '#00f0ff', textShadow: '0 0 10px rgba(0,240,255,0.5)' }}>解决方案</span>
|
||||
</Title>
|
||||
</motion.div>
|
||||
<Paragraph style={{ color: '#888', maxWidth: 700, margin: '0 auto', fontSize: 16 }}>
|
||||
从数据处理到模型部署,我们为您提供一站式 AI 基础设施服务。
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<Row gutter={[32, 32]} justify="center">
|
||||
{services.map((item, index) => (
|
||||
<Col xs={24} md={8} key={item.id}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.2, duration: 0.5 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
onClick={() => navigate(`/services/${item.id}`)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div
|
||||
className="glass-panel"
|
||||
style={{
|
||||
padding: 30,
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
border: `1px solid ${item.color}33`,
|
||||
boxShadow: `0 0 20px ${item.color}11`
|
||||
}}
|
||||
>
|
||||
{/* HUD 装饰线 */}
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, width: 20, height: 2, background: item.color }} />
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, width: 2, height: 20, background: item.color }} />
|
||||
<div style={{ position: 'absolute', bottom: 0, right: 0, width: 20, height: 2, background: item.color }} />
|
||||
<div style={{ position: 'absolute', bottom: 0, right: 0, width: 2, height: 20, background: item.color }} />
|
||||
|
||||
<div style={{ marginBottom: 20, display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{
|
||||
width: 60, height: 60,
|
||||
borderRadius: '50%',
|
||||
background: `${item.color}22`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
marginRight: 15,
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{item.display_icon ? (
|
||||
<img src={item.display_icon} alt={item.title} style={{ width: '60%', height: '60%', objectFit: 'contain' }} />
|
||||
) : (
|
||||
<div style={{ width: 30, height: 30, background: item.color, borderRadius: '50%' }} />
|
||||
)}
|
||||
</div>
|
||||
<h3 style={{ margin: 0, fontSize: 22, color: '#fff' }}>{item.title}</h3>
|
||||
</div>
|
||||
|
||||
<p style={{ color: '#ccc', lineHeight: 1.6, minHeight: 60 }}>{item.description}</p>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
{item.features_list && item.features_list.map((feat, i) => (
|
||||
<div key={i} style={{
|
||||
display: 'flex', alignItems: 'center', marginBottom: 8, color: item.color
|
||||
}}>
|
||||
<div style={{ width: 6, height: 6, background: item.color, marginRight: 10, borderRadius: '50%' }} />
|
||||
{feat}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="link"
|
||||
style={{ padding: 0, marginTop: 20, color: '#fff' }}
|
||||
icon={<RightOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/services/${item.id}`);
|
||||
}}
|
||||
>
|
||||
了解更多
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 动态流程图模拟 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8 }}
|
||||
style={{ marginTop: 80, padding: 40, background: 'rgba(0,0,0,0.3)', borderRadius: 20, border: '1px dashed #333', textAlign: 'center' }}
|
||||
>
|
||||
<Title level={3} style={{ color: '#fff', marginBottom: 40 }}>服务流程</Title>
|
||||
<Row justify="space-around" align="middle" gutter={[20, 20]}>
|
||||
{['需求分析', '数据准备', '模型训练', '测试验证', '私有化部署'].map((step, i) => (
|
||||
<Col key={i} xs={12} md={4}>
|
||||
<div style={{
|
||||
width: '100%', aspectRatio: '1',
|
||||
border: '2px solid #333', borderRadius: '50%',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: '#888', fontSize: 16, fontWeight: 'bold',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{step}
|
||||
{/* 简单的连接线模拟 */}
|
||||
{i < 4 && (
|
||||
<div className="process-arrow" style={{
|
||||
position: 'absolute', right: -20, top: '50%',
|
||||
width: 20, height: 2, background: '#333',
|
||||
display: 'none' // 移动端隐藏
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<style>{`
|
||||
@media (min-width: 768px) {
|
||||
.process-arrow { display: block !important; }
|
||||
}
|
||||
`}</style>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIServices;
|
||||
111
frontend/src/pages/ARExperience.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Button, Typography, Spin, Row, Col, Empty } from 'antd';
|
||||
import { ScanOutlined } from '@ant-design/icons';
|
||||
import { getARServices } from '../api';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const ARExperience = () => {
|
||||
const [scanning, setScanning] = useState(true);
|
||||
const [arServices, setArServices] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAR = async () => {
|
||||
try {
|
||||
const res = await getARServices();
|
||||
setArServices(res.data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch AR services:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
fetchAR();
|
||||
}, []);
|
||||
|
||||
if (loading) return <div style={{ textAlign: 'center', padding: 100 }}><Spin size="large" /></div>;
|
||||
|
||||
return (
|
||||
<div style={{ padding: '40px 0', minHeight: '80vh', position: 'relative' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 60, position: 'relative', zIndex: 2 }}>
|
||||
<Title level={1} style={{ color: '#fff', letterSpacing: 4 }}>
|
||||
AR <span style={{ color: '#00f0ff' }}>UNIVERSE</span>
|
||||
</Title>
|
||||
<Paragraph style={{ color: '#aaa', fontSize: 18, maxWidth: 600, margin: '0 auto' }}>
|
||||
探索全息增强现实体验。请佩戴您的设备,或使用移动端摄像头扫描空间。
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{arServices.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', marginTop: 100, zIndex: 2, position: 'relative' }}>
|
||||
<Empty description={<span style={{ color: '#666' }}>暂无 AR 体验内容</span>} />
|
||||
</div>
|
||||
) : (
|
||||
<Row gutter={[32, 32]} justify="center" style={{ padding: '0 20px', position: 'relative', zIndex: 2 }}>
|
||||
{arServices.map((item, index) => (
|
||||
<Col xs={24} md={12} lg={8} key={item.id}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
>
|
||||
<div style={{
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
border: '1px solid rgba(0,240,255,0.2)',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
height: '100%'
|
||||
}}>
|
||||
<div style={{ height: 200, background: '#000', overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{item.display_cover_image ? (
|
||||
<img src={item.display_cover_image} alt={item.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
) : (
|
||||
<ScanOutlined style={{ fontSize: 40, color: '#333' }} />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: 20 }}>
|
||||
<h3 style={{ color: '#fff', fontSize: 20 }}>{item.title}</h3>
|
||||
<p style={{ color: '#888', marginBottom: 20, minHeight: 44 }}>{item.description}</p>
|
||||
<Button type="primary" block ghost style={{ borderColor: '#00f0ff', color: '#00f0ff' }}>
|
||||
启动体验
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* 装饰性背景 */}
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: `
|
||||
radial-gradient(circle at 50% 50%, rgba(0, 240, 255, 0.05) 0%, transparent 50%)
|
||||
`,
|
||||
zIndex: 0,
|
||||
pointerEvents: 'none'
|
||||
}} />
|
||||
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
height: '300px',
|
||||
background: `linear-gradient(to top, rgba(0,0,0,0.8), transparent)`,
|
||||
zIndex: 1,
|
||||
pointerEvents: 'none'
|
||||
}} />
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ARExperience;
|
||||
25
frontend/src/pages/Home.css
Normal file
@@ -0,0 +1,25 @@
|
||||
.tech-card {
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
border: 1px solid #303030 !important;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tech-card:hover {
|
||||
border-color: #00b96b !important;
|
||||
box-shadow: 0 0 15px rgba(0, 185, 107, 0.3);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.tech-card-title {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tech-price {
|
||||
color: #00b96b;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
154
frontend/src/pages/Home.jsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Row, Col, Tag, Button, Spin, Typography } from 'antd';
|
||||
import { RocketOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { getConfigs } from '../api';
|
||||
import './Home.css';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const Home = () => {
|
||||
const [products, setProducts] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [typedText, setTypedText] = useState('');
|
||||
const fullText = "未来已来 AI 核心驱动";
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
let i = 0;
|
||||
const typingInterval = setInterval(() => {
|
||||
if (i < fullText.length) {
|
||||
setTypedText(prev => prev + fullText.charAt(i));
|
||||
i++;
|
||||
} else {
|
||||
clearInterval(typingInterval);
|
||||
}
|
||||
}, 150);
|
||||
|
||||
return () => clearInterval(typingInterval);
|
||||
}, []);
|
||||
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
const response = await getConfigs();
|
||||
setProducts(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch products:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cardVariants = {
|
||||
hidden: { opacity: 0, y: 50 },
|
||||
visible: (i) => ({
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: i * 0.1,
|
||||
duration: 0.5,
|
||||
type: "spring",
|
||||
stiffness: 100
|
||||
}
|
||||
}),
|
||||
hover: {
|
||||
scale: 1.05,
|
||||
rotateX: 5,
|
||||
rotateY: 5,
|
||||
boxShadow: "0px 10px 30px rgba(0, 185, 107, 0.4)",
|
||||
transition: { duration: 0.3 }
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
|
||||
<Spin size="large" tip="加载硬件配置中..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ textAlign: 'center', marginBottom: 60, marginTop: 40 }}>
|
||||
<Title level={1} style={{ color: '#fff', fontSize: 'clamp(2rem, 5vw, 4rem)', marginBottom: 20, minHeight: '60px' }}>
|
||||
<span className="neon-text-green">{typedText}</span><span className="cursor-blink">|</span>
|
||||
</Title>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 2, duration: 1 }}
|
||||
>
|
||||
<Paragraph style={{ color: '#aaa', fontSize: '18px', maxWidth: 600, margin: '0 auto', lineHeight: '1.6' }}>
|
||||
量迹 AI 硬件为您提供最强大的边缘计算能力,搭载最新一代神经处理单元,赋能您的每一个创意。
|
||||
</Paragraph>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<Row gutter={[24, 24]}>
|
||||
{products.map((product, index) => (
|
||||
<Col xs={24} sm={12} md={8} lg={6} key={product.id}>
|
||||
<motion.div
|
||||
custom={index}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
whileHover="hover"
|
||||
variants={cardVariants}
|
||||
style={{ perspective: 1000 }}
|
||||
>
|
||||
<Card
|
||||
className="tech-card glass-panel"
|
||||
bordered={false}
|
||||
cover={
|
||||
<div style={{
|
||||
height: 200,
|
||||
background: 'linear-gradient(135deg, rgba(31,31,31,0.8), rgba(42,42,42,0.8))',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: '#444',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.05)'
|
||||
}}>
|
||||
<motion.div
|
||||
animate={{ y: [0, -10, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
|
||||
>
|
||||
<RocketOutlined style={{ fontSize: 60, color: '#00b96b' }} />
|
||||
</motion.div>
|
||||
</div>
|
||||
}
|
||||
onClick={() => navigate(`/product/${product.id}`)}
|
||||
>
|
||||
<div className="tech-card-title neon-text-blue">{product.name}</div>
|
||||
<div style={{ marginBottom: 10, height: 40, overflow: 'hidden', color: '#bbb' }}>
|
||||
{product.description}
|
||||
</div>
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<Tag color="cyan" style={{ background: 'rgba(0,255,255,0.1)', border: '1px solid cyan' }}>{product.chip_type}</Tag>
|
||||
{product.has_camera && <Tag color="blue" style={{ background: 'rgba(0,0,255,0.1)', border: '1px solid blue' }}>Camera</Tag>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div className="tech-price neon-text-green">¥{product.price}</div>
|
||||
<Button type="primary" shape="circle" icon={<RightOutlined />} style={{ background: '#00b96b', borderColor: '#00b96b' }} />
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<style>{`
|
||||
.cursor-blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
52
frontend/src/pages/Payment.css
Normal file
@@ -0,0 +1,52 @@
|
||||
.payment-container {
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
padding: 40px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid #303030;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.payment-title {
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.payment-amount {
|
||||
font-size: 48px;
|
||||
color: #00b96b;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.payment-info {
|
||||
text-align: left;
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.payment-method {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.payment-method-item {
|
||||
border: 1px solid #444;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.payment-method-item.active {
|
||||
border-color: #00b96b;
|
||||
background: rgba(0, 185, 107, 0.1);
|
||||
}
|
||||
183
frontend/src/pages/Payment.jsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Button, message, Result, Spin, QRCode } from 'antd';
|
||||
import { WechatOutlined, AlipayCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||
import { getOrder, initiatePayment, confirmPayment } from '../api';
|
||||
import './Payment.css';
|
||||
|
||||
const Payment = () => {
|
||||
const { orderId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [order, setOrder] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [paying, setPaying] = useState(false);
|
||||
const [paySuccess, setPaySuccess] = useState(false);
|
||||
const [paymentMethod, setPaymentMethod] = useState('wechat');
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrder();
|
||||
}, [orderId]);
|
||||
|
||||
const fetchOrder = async () => {
|
||||
try {
|
||||
const response = await getOrder(orderId);
|
||||
setOrder(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch order:', error);
|
||||
// Fallback if getOrder API fails (404/405), we might show basic info or error
|
||||
// Assuming for now it works or we handle it
|
||||
message.error('无法获取订单信息,请重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePay = async () => {
|
||||
if (paymentMethod === 'alipay') {
|
||||
message.info('暂未开通支付宝支付,请使用微信支付');
|
||||
return;
|
||||
}
|
||||
|
||||
setPaying(true);
|
||||
try {
|
||||
// 1. 获取微信支付参数
|
||||
const response = await initiatePayment(orderId);
|
||||
const payData = response.data;
|
||||
|
||||
if (typeof WeixinJSBridge === 'undefined') {
|
||||
message.warning('请在微信内置浏览器中打开以完成支付');
|
||||
setPaying(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 调用微信支付
|
||||
const onBridgeReady = () => {
|
||||
window.WeixinJSBridge.invoke(
|
||||
'getBrandWCPayRequest', {
|
||||
"appId": payData.appId, // 公众号名称,由商户传入
|
||||
"timeStamp": payData.timeStamp, // 时间戳,自1970年以来的秒数
|
||||
"nonceStr": payData.nonceStr, // 随机串
|
||||
"package": payData.package,
|
||||
"signType": payData.signType, // 微信签名方式:
|
||||
"paySign": payData.paySign // 微信签名
|
||||
},
|
||||
function(res) {
|
||||
setPaying(false);
|
||||
if (res.err_msg == "get_brand_wcpay_request:ok") {
|
||||
message.success('支付成功!');
|
||||
setPaySuccess(true);
|
||||
// 这里可以再次调用后端查询接口确认状态,但通常 JSAPI 回调 ok 即可认为成功
|
||||
// 为了保险,可以去轮询一下后端状态,或者直接展示成功页
|
||||
} else if (res.err_msg == "get_brand_wcpay_request:cancel") {
|
||||
message.info('支付已取消');
|
||||
} else {
|
||||
message.error('支付失败,请重试');
|
||||
console.error('WeChat Pay Error:', res);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (typeof window.WeixinJSBridge == "undefined") {
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
|
||||
} else if (document.attachEvent) {
|
||||
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
|
||||
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
|
||||
}
|
||||
} else {
|
||||
onBridgeReady();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error.response && error.response.data && error.response.data.error) {
|
||||
message.error(error.response.data.error);
|
||||
} else {
|
||||
message.error('支付发起失败,请稍后重试');
|
||||
}
|
||||
setPaying(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" /></div>;
|
||||
|
||||
if (paySuccess) {
|
||||
return (
|
||||
<div className="payment-container" style={{ borderColor: '#00b96b' }}>
|
||||
<Result
|
||||
status="success"
|
||||
icon={<CheckCircleOutlined style={{ color: '#00b96b' }} />}
|
||||
title={<span style={{ color: '#fff' }}>支付成功</span>}
|
||||
subTitle={<span style={{ color: '#888' }}>订单 {orderId} 已完成支付,我们将尽快为您发货。</span>}
|
||||
extra={[
|
||||
<Button type="primary" key="home" onClick={() => navigate('/')}>
|
||||
返回首页
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="payment-container">
|
||||
<div className="payment-title">收银台</div>
|
||||
|
||||
{order ? (
|
||||
<>
|
||||
<div className="payment-amount">¥{order.total_price}</div>
|
||||
<div className="payment-info">
|
||||
<p><strong>订单编号:</strong> {order.id}</p>
|
||||
<p><strong>商品名称:</strong> {order.config_name || 'AI 硬件设备'}</p>
|
||||
<p><strong>收货人:</strong> {order.customer_name}</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="payment-info">
|
||||
<p>订单 ID: {orderId}</p>
|
||||
<p>无法加载详情,但您可以尝试支付。</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ color: '#fff', marginBottom: 15, textAlign: 'left' }}>选择支付方式:</div>
|
||||
<div className="payment-method">
|
||||
<div
|
||||
className={`payment-method-item ${paymentMethod === 'wechat' ? 'active' : ''}`}
|
||||
onClick={() => setPaymentMethod('wechat')}
|
||||
>
|
||||
<WechatOutlined style={{ color: '#09BB07', fontSize: 24, verticalAlign: 'middle', marginRight: 8 }} />
|
||||
微信支付
|
||||
</div>
|
||||
<div
|
||||
className={`payment-method-item ${paymentMethod === 'alipay' ? 'active' : ''}`}
|
||||
onClick={() => setPaymentMethod('alipay')}
|
||||
>
|
||||
<AlipayCircleOutlined style={{ color: '#1677FF', fontSize: 24, verticalAlign: 'middle', marginRight: 8 }} />
|
||||
支付宝
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{paying && (
|
||||
<div style={{ margin: '20px 0', padding: 20, background: '#fff', borderRadius: 8, display: 'inline-block' }}>
|
||||
<QRCode value={`mock-payment-${orderId}`} bordered={false} />
|
||||
<p style={{ color: '#000', marginTop: 10 }}>请扫码支付 (模拟)</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!paying && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
onClick={handlePay}
|
||||
style={{ height: 50, fontSize: 18, background: paymentMethod === 'wechat' ? '#09BB07' : '#1677FF' }}
|
||||
>
|
||||
立即支付
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Payment;
|
||||