更新admin 和 swagger

This commit is contained in:
jeremygan2021
2026-02-02 14:32:24 +08:00
parent 6af90017d5
commit c93bf9ef11
26 changed files with 407 additions and 53 deletions

View File

@@ -32,6 +32,7 @@ ALLOWED_HOSTS = ["*"]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'unfold', # django-unfold必须在admin之前
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@@ -40,6 +41,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'rest_framework', 'rest_framework',
'corsheaders', 'corsheaders',
'drf_spectacular', # Swagger文档生成
'shop', 'shop',
] ]
@@ -126,3 +128,54 @@ USE_TZ = True
# https://docs.djangoproject.com/en/6.0/howto/static-files/ # https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/' STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
# 静态文件配置
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
# Django REST Framework配置
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_AUTHENTICATION_CLASSES': [],
'DEFAULT_PERMISSION_CLASSES': [],
}
# drf-spectacular配置
SPECTACULAR_SETTINGS = {
'TITLE': '科技公司产品购买API',
'DESCRIPTION': '科技公司产品购买官网的API文档',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': True,
'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'],
'COMPONENT_SPLIT_REQUEST': True,
'SCHEMA_PATH_PREFIX': r'/api/v[0-9]',
'SWAGGER_UI_SETTINGS': {
'deepLinking': True,
'persistAuthorization': True,
'displayOperationId': True,
},
}
# django-unfold配置
UNFOLD = {
"SITE_TITLE": "科技公司产品管理",
"SITE_HEADER": "科技公司产品购买系统",
"SITE_URL": "/",
"COLORS": {
"primary": {
"50": "rgb(240 249 255)",
"100": "rgb(224 242 254)",
"200": "rgb(186 230 253)",
"300": "rgb(125 211 252)",
"400": "rgb(56 189 248)",
"500": "rgb(14 165 233)",
"600": "rgb(2 132 199)",
"700": "rgb(3 105 161)",
"800": "rgb(7 89 133)",
"900": "rgb(12 74 110)",
"950": "rgb(8 47 73)",
},
},
}

View File

@@ -1,7 +1,19 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/', include('shop.urls')), path('api/', include('shop.urls')),
# Swagger文档路由
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
] ]
# 静态文件配置(开发环境)
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@@ -1,8 +1,18 @@
asgiref==3.11.0 asgiref==3.11.0
attrs==25.4.0
Django==6.0.1 Django==6.0.1
django-cors-headers==4.9.0 django-cors-headers==4.9.0
django-unfold==0.77.1
djangorestframework==3.16.1 djangorestframework==3.16.1
drf-spectacular==0.29.0
inflection==0.5.1
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
pillow==12.1.0 pillow==12.1.0
psycopg2-binary==2.9.11 psycopg2-binary==2.9.11
PyYAML==6.0.3
qrcode==8.2 qrcode==8.2
referencing==0.37.0
rpds-py==0.30.0
sqlparse==0.5.5 sqlparse==0.5.5
uritemplate==4.2.0

View File

@@ -1,6 +1,8 @@
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html from django.utils.html import format_html
from django.db.models import Sum from django.db.models import Sum
from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import display
from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService, ProductFeature from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService, ProductFeature
import qrcode import qrcode
from io import BytesIO from io import BytesIO
@@ -11,13 +13,13 @@ admin.site.site_header = "量迹AI硬件销售管理后台"
admin.site.site_title = "量迹AI后台" admin.site.site_title = "量迹AI后台"
admin.site.index_title = "欢迎使用量迹AI管理系统" admin.site.index_title = "欢迎使用量迹AI管理系统"
class ProductFeatureInline(admin.TabularInline): class ProductFeatureInline(TabularInline):
model = ProductFeature model = ProductFeature
extra = 1 extra = 1
fields = ('title', 'description', 'icon_name', 'icon_image', 'icon_url', 'order') fields = ('title', 'description', 'icon_name', 'icon_image', 'icon_url', 'order')
@admin.register(WeChatPayConfig) @admin.register(WeChatPayConfig)
class WeChatPayConfigAdmin(admin.ModelAdmin): class WeChatPayConfigAdmin(ModelAdmin):
list_display = ('app_id', 'mch_id', 'is_active', 'notify_url') list_display = ('app_id', 'mch_id', 'is_active', 'notify_url')
list_filter = ('is_active',) list_filter = ('is_active',)
search_fields = ('app_id', 'mch_id') search_fields = ('app_id', 'mch_id')
@@ -34,7 +36,7 @@ class WeChatPayConfigAdmin(admin.ModelAdmin):
) )
@admin.register(ESP32Config) @admin.register(ESP32Config)
class ESP32ConfigAdmin(admin.ModelAdmin): class ESP32ConfigAdmin(ModelAdmin):
list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone') list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone')
list_filter = ('chip_type', 'has_camera') list_filter = ('chip_type', 'has_camera')
search_fields = ('name', 'description') search_fields = ('name', 'description')
@@ -53,13 +55,16 @@ class ESP32ConfigAdmin(admin.ModelAdmin):
) )
@admin.register(Service) @admin.register(Service)
class ServiceAdmin(admin.ModelAdmin): class ServiceAdmin(ModelAdmin):
list_display = ('title', 'created_at') list_display = ('title', 'created_at')
search_fields = ('title', 'description') search_fields = ('title', 'description')
fieldsets = ( fieldsets = (
('基本信息', { ('基本信息', {
'fields': ('title', 'description', 'color') 'fields': ('title', 'description', 'color')
}), }),
('价格与交付', {
'fields': ('price', 'unit', 'delivery_time', 'delivery_content')
}),
('图标', { ('图标', {
'fields': ('icon', 'icon_url'), 'fields': ('icon', 'icon_url'),
'description': '图标上传和URL二选一优先使用URL' 'description': '图标上传和URL二选一优先使用URL'
@@ -74,7 +79,7 @@ class ServiceAdmin(admin.ModelAdmin):
) )
@admin.register(ARService) @admin.register(ARService)
class ARServiceAdmin(admin.ModelAdmin): class ARServiceAdmin(ModelAdmin):
list_display = ('title', 'created_at') list_display = ('title', 'created_at')
search_fields = ('title', 'description') search_fields = ('title', 'description')
fieldsets = ( fieldsets = (
@@ -88,7 +93,7 @@ class ARServiceAdmin(admin.ModelAdmin):
) )
@admin.register(Salesperson) @admin.register(Salesperson)
class SalespersonAdmin(admin.ModelAdmin): class SalespersonAdmin(ModelAdmin):
list_display = ('name', 'code', 'total_sales', 'view_promotion_url') list_display = ('name', 'code', 'total_sales', 'view_promotion_url')
search_fields = ('name', 'code') search_fields = ('name', 'code')
readonly_fields = ('promotion_qr_code', 'promotion_url_display', 'total_sales_display') readonly_fields = ('promotion_qr_code', 'promotion_url_display', 'total_sales_display')
@@ -100,12 +105,11 @@ class SalespersonAdmin(admin.ModelAdmin):
) )
return queryset return queryset
@display(description="累计销售额 (已支付)", ordering='_total_sales')
def total_sales(self, obj): def total_sales(self, obj):
# 仅计算已支付的订单 # 仅计算已支付的订单
paid_sales = obj.orders.filter(status='paid').aggregate(total=Sum('total_price'))['total'] paid_sales = obj.orders.filter(status='paid').aggregate(total=Sum('total_price'))['total']
return f"¥{paid_sales or 0:.2f}" return f"¥{paid_sales or 0:.2f}"
total_sales.short_description = "累计销售额 (已支付)"
total_sales.admin_order_field = '_total_sales'
def total_sales_display(self, obj): def total_sales_display(self, obj):
return self.total_sales(obj) return self.total_sales(obj)
@@ -116,15 +120,16 @@ class SalespersonAdmin(admin.ModelAdmin):
base_url = "http://localhost:5173" base_url = "http://localhost:5173"
return f"{base_url}/?ref={obj.code}" return f"{base_url}/?ref={obj.code}"
@display(description="推广链接")
def view_promotion_url(self, obj): def view_promotion_url(self, obj):
url = self.promotion_url(obj) url = self.promotion_url(obj)
return format_html('<a href="{}" target="_blank">打开推广链接</a>', url) return format_html('<a href="{}" target="_blank" class="button">打开推广链接</a>', url)
view_promotion_url.short_description = "推广链接"
def promotion_url_display(self, obj): def promotion_url_display(self, obj):
return self.promotion_url(obj) return self.promotion_url(obj)
promotion_url_display.short_description = "完整推广链接" promotion_url_display.short_description = "完整推广链接"
@display(description="推广二维码")
def promotion_qr_code(self, obj): def promotion_qr_code(self, obj):
if not obj.code: if not obj.code:
return "请先保存以生成二维码" return "请先保存以生成二维码"
@@ -144,9 +149,7 @@ class SalespersonAdmin(admin.ModelAdmin):
img.save(buffer, format="PNG") img.save(buffer, format="PNG")
img_str = base64.b64encode(buffer.getvalue()).decode() img_str = base64.b64encode(buffer.getvalue()).decode()
return format_html('<img src="data:image/png;base64,{}" width="200" height="200" />', img_str) return format_html('<img src="data:image/png;base64,{}" width="200" height="200" class="qr-code" />', img_str)
promotion_qr_code.short_description = "推广二维码"
fieldsets = ( fieldsets = (
('基本信息', { ('基本信息', {
@@ -161,7 +164,7 @@ class SalespersonAdmin(admin.ModelAdmin):
) )
@admin.register(Order) @admin.register(Order)
class OrderAdmin(admin.ModelAdmin): class OrderAdmin(ModelAdmin):
list_display = ('id', 'customer_name', 'config', 'total_price', 'status', 'salesperson', 'created_at') list_display = ('id', 'customer_name', 'config', 'total_price', 'status', 'salesperson', 'created_at')
list_filter = ('status', 'salesperson', 'created_at') list_filter = ('status', 'salesperson', 'created_at')
search_fields = ('id', 'customer_name', 'phone_number', 'wechat_trade_no') search_fields = ('id', 'customer_name', 'phone_number', 'wechat_trade_no')

View File

@@ -0,0 +1,55 @@
# Generated by Django 6.0.1 on 2026-02-02 06:16
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0007_productfeature'),
]
operations = [
migrations.AddField(
model_name='service',
name='delivery_content',
field=models.TextField(blank=True, help_text='描述将交付给客户的具体成果', verbose_name='交付内容'),
),
migrations.AddField(
model_name='service',
name='delivery_time',
field=models.CharField(blank=True, help_text='例如3-5个工作日', max_length=50, verbose_name='预计交付周期'),
),
migrations.AddField(
model_name='service',
name='price',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='起步价格'),
),
migrations.AddField(
model_name='service',
name='unit',
field=models.CharField(default='', help_text='例如:次、小时、月、个', max_length=20, verbose_name='计费单位'),
),
migrations.CreateModel(
name='ServiceOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('customer_name', models.CharField(max_length=100, verbose_name='客户姓名')),
('company_name', models.CharField(blank=True, max_length=100, verbose_name='公司名称')),
('phone_number', models.CharField(max_length=20, verbose_name='联系电话')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='电子邮箱')),
('requirements', models.TextField(blank=True, verbose_name='具体需求描述')),
('total_price', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='预估总价')),
('status', models.CharField(choices=[('pending', '待沟通/待支付'), ('processing', '服务进行中'), ('completed', '已完成'), ('cancelled', '已取消')], default='pending', max_length=20, verbose_name='订单状态')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('salesperson', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.salesperson', verbose_name='所属销售员')),
('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.service', verbose_name='所选服务')),
],
options={
'verbose_name': '服务订单',
'verbose_name_plural': '服务订单列表',
},
),
]

View File

@@ -137,6 +137,10 @@ class Service(models.Model):
icon_url = models.URLField(blank=True, null=True, verbose_name="图标 (URL)") icon_url = models.URLField(blank=True, null=True, verbose_name="图标 (URL)")
description = models.TextField(verbose_name="简介") description = models.TextField(verbose_name="简介")
features = models.TextField(verbose_name="特性列表", help_text="每行一个特性") features = models.TextField(verbose_name="特性列表", help_text="每行一个特性")
price = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="起步价格")
unit = models.CharField(max_length=20, default="", verbose_name="计费单位", help_text="例如:次、小时、月、个")
delivery_time = models.CharField(max_length=50, blank=True, verbose_name="预计交付周期", help_text="例如3-5个工作日")
delivery_content = models.TextField(blank=True, verbose_name="交付内容", help_text="描述将交付给客户的具体成果")
color = models.CharField(max_length=20, default="#00f0ff", verbose_name="主题色") 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 = models.ImageField(upload_to='services/details/', blank=True, null=True, verbose_name="详情页长图 (上传)")
detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)") detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)")
@@ -150,6 +154,39 @@ class Service(models.Model):
verbose_name_plural = "AI服务管理" verbose_name_plural = "AI服务管理"
class ServiceOrder(models.Model):
"""
AI服务订单模型
"""
STATUS_CHOICES = (
('pending', '待沟通/待支付'),
('processing', '服务进行中'),
('completed', '已完成'),
('cancelled', '已取消'),
)
service = models.ForeignKey(Service, on_delete=models.CASCADE, verbose_name="所选服务")
customer_name = models.CharField(max_length=100, verbose_name="客户姓名")
company_name = models.CharField(max_length=100, blank=True, verbose_name="公司名称")
phone_number = models.CharField(max_length=20, verbose_name="联系电话")
email = models.EmailField(blank=True, verbose_name="电子邮箱")
requirements = models.TextField(verbose_name="具体需求描述", blank=True)
total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="预估总价", default=0)
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="所属销售员")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
def __str__(self):
return f"{self.customer_name} - {self.service.title}"
class Meta:
verbose_name = "服务订单"
verbose_name_plural = "服务订单列表"
class ARService(models.Model): class ARService(models.Model):
""" """
AR体验服务模型 AR体验服务模型

View File

@@ -1,5 +1,5 @@
from rest_framework import serializers from rest_framework import serializers
from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature, ServiceOrder
class ProductFeatureSerializer(serializers.ModelSerializer): class ProductFeatureSerializer(serializers.ModelSerializer):
""" """
@@ -49,6 +49,38 @@ class ServiceSerializer(serializers.ModelSerializer):
return obj.detail_image.url return obj.detail_image.url
return None return None
class ServiceOrderSerializer(serializers.ModelSerializer):
"""
AI服务订单序列化器
"""
service_name = serializers.CharField(source='service.title', read_only=True)
# 接收前端传来的 ref_code
ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True)
class Meta:
model = ServiceOrder
fields = ['id', 'service', 'service_name', 'customer_name', 'company_name',
'phone_number', 'email', 'requirements', 'total_price', 'status', 'created_at', 'ref_code']
read_only_fields = ['total_price', 'status', 'created_at']
def create(self, validated_data):
ref_code = validated_data.pop('ref_code', None)
service = validated_data.get('service')
# 默认设置预估总价为服务起步价
if service:
validated_data['total_price'] = service.price
# 尝试关联销售员
if ref_code:
try:
salesperson = Salesperson.objects.get(code=ref_code)
validated_data['salesperson'] = salesperson
except Salesperson.DoesNotExist:
pass
return super().create(validated_data)
class ARServiceSerializer(serializers.ModelSerializer): class ARServiceSerializer(serializers.ModelSerializer):
""" """
AR服务序列化器 AR服务序列化器

View File

@@ -1,12 +1,13 @@
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .views import ESP32ConfigViewSet, OrderViewSet, order_check_view, ServiceViewSet, ARServiceViewSet from .views import ESP32ConfigViewSet, OrderViewSet, order_check_view, ServiceViewSet, ARServiceViewSet, ServiceOrderViewSet
router = DefaultRouter() router = DefaultRouter()
router.register(r'configs', ESP32ConfigViewSet) router.register(r'configs', ESP32ConfigViewSet)
router.register(r'orders', OrderViewSet) router.register(r'orders', OrderViewSet)
router.register(r'services', ServiceViewSet) router.register(r'services', ServiceViewSet)
router.register(r'ar', ARServiceViewSet) router.register(r'ar', ARServiceViewSet)
router.register(r'service-orders', ServiceOrderViewSet)
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),

View File

@@ -2,8 +2,8 @@ from rest_framework import viewsets, status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from django.shortcuts import render from django.shortcuts import render
from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService, ServiceOrder
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer, ServiceOrderSerializer
class ARServiceViewSet(viewsets.ReadOnlyModelViewSet): class ARServiceViewSet(viewsets.ReadOnlyModelViewSet):
""" """
@@ -28,6 +28,13 @@ class ServiceViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Service.objects.all().order_by('-created_at') queryset = Service.objects.all().order_by('-created_at')
serializer_class = ServiceSerializer serializer_class = ServiceSerializer
class ServiceOrderViewSet(viewsets.ModelViewSet):
"""
AI服务订单管理
"""
queryset = ServiceOrder.objects.all()
serializer_class = ServiceOrderSerializer
class ESP32ConfigViewSet(viewsets.ReadOnlyModelViewSet): class ESP32ConfigViewSet(viewsets.ReadOnlyModelViewSet):
""" """
提供ESP32配置选项的列表和详情 提供ESP32配置选项的列表和详情

View File

@@ -16,6 +16,7 @@ export const confirmPayment = (orderId) => api.post(`/orders/${orderId}/confirm_
export const getServices = () => api.get('/services/'); export const getServices = () => api.get('/services/');
export const getServiceDetail = (id) => api.get(`/services/${id}/`); export const getServiceDetail = (id) => api.get(`/services/${id}/`);
export const createServiceOrder = (data) => api.post('/service-orders/', data);
export const getARServices = () => api.get('/ar/'); export const getARServices = () => api.get('/ar/');
export default api; export default api;

View File

@@ -19,10 +19,9 @@ const Home = () => {
fetchProducts(); fetchProducts();
let i = 0; let i = 0;
const typingInterval = setInterval(() => { const typingInterval = setInterval(() => {
if (i < fullText.length) { i++;
setTypedText(prev => prev + fullText.charAt(i)); setTypedText(fullText.slice(0, i));
i++; if (i >= fullText.length) {
} else {
clearInterval(typingInterval); clearInterval(typingInterval);
} }
}, 150); }, 150);
@@ -73,6 +72,26 @@ const Home = () => {
return ( return (
<div> <div>
<div style={{ textAlign: 'center', marginBottom: 60, marginTop: 40 }}> <div style={{ textAlign: 'center', marginBottom: 60, marginTop: 40 }}>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1 }}
style={{ marginBottom: 30 }}
>
<motion.img
src="/gXEu5E01.svg"
alt="LiangJi Tech Logo"
animate={{
filter: [
'invert(1) drop-shadow(0 0 10px rgba(0, 240, 255, 0.3))',
'invert(1) drop-shadow(0 0 20px rgba(0, 240, 255, 0.7))',
'invert(1) drop-shadow(0 0 10px rgba(0, 240, 255, 0.3))'
]
}}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
style={{ width: 180, height: 'auto' }}
/>
</motion.div>
<Title level={1} style={{ color: '#fff', fontSize: 'clamp(2rem, 5vw, 4rem)', marginBottom: 20, minHeight: '60px' }}> <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> <span className="neon-text-green">{typedText}</span><span className="cursor-blink">|</span>
</Title> </Title>

View File

@@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { Typography, Button, Spin, Empty } from 'antd'; import { Typography, Button, Spin, Empty, Descriptions, Tag, Row, Col, Modal, Form, Input, message, Statistic } from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons'; import { ArrowLeftOutlined, ClockCircleOutlined, GiftOutlined, ShoppingCartOutlined } from '@ant-design/icons';
import { getServiceDetail } from '../api'; import { getServiceDetail, createServiceOrder } from '../api';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
const { Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;
@@ -12,6 +12,9 @@ const ServiceDetail = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [service, setService] = useState(null); const [service, setService] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [form] = Form.useForm();
useEffect(() => { useEffect(() => {
const fetchDetail = async () => { const fetchDetail = async () => {
@@ -27,6 +30,28 @@ const ServiceDetail = () => {
fetchDetail(); fetchDetail();
}, [id]); }, [id]);
const handlePurchase = async (values) => {
setSubmitting(true);
try {
const orderData = {
service: service.id,
customer_name: values.customer_name,
company_name: values.company_name,
phone_number: values.phone_number,
email: values.email,
requirements: values.requirements
};
await createServiceOrder(orderData);
message.success('需求已提交,我们的销售顾问将尽快与您联系!');
setIsModalOpen(false);
} catch (error) {
console.error(error);
message.error('提交失败,请重试');
} finally {
setSubmitting(false);
}
};
if (loading) { if (loading) {
return ( return (
<div style={{ textAlign: 'center', padding: '100px 0' }}> <div style={{ textAlign: 'center', padding: '100px 0' }}>
@@ -62,36 +87,135 @@ const ServiceDetail = () => {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
> >
<div style={{ textAlign: 'center', marginBottom: 40 }}> <Row gutter={[40, 40]}>
<Title level={1} style={{ color: '#fff' }}> <Col xs={24} md={16}>
{service.title} <div style={{ textAlign: 'left', marginBottom: 40 }}>
</Title> <Title level={1} style={{ color: '#fff' }}>
<Paragraph style={{ color: '#888', fontSize: 18, maxWidth: 800, margin: '0 auto' }}> {service.title}
{service.description} </Title>
</Paragraph> <Paragraph style={{ color: '#888', fontSize: 18 }}>
</div> {service.description}
</Paragraph>
<div style={{ marginTop: 30, background: 'rgba(255,255,255,0.05)', padding: 20, borderRadius: 12 }}>
<Title level={4} style={{ color: '#fff' }}>服务详情</Title>
<Descriptions column={1} labelStyle={{ color: '#888' }} contentStyle={{ color: '#fff' }}>
<Descriptions.Item label={<span style={{ display: 'flex', alignItems: 'center' }}><ClockCircleOutlined style={{ marginRight: 8 }} /> 交付周期</span>}>
{service.delivery_time || '待沟通'}
</Descriptions.Item>
<Descriptions.Item label={<span style={{ display: 'flex', alignItems: 'center' }}><GiftOutlined style={{ marginRight: 8 }} /> 交付内容</span>}>
{service.delivery_content || '根据需求定制'}
</Descriptions.Item>
</Descriptions>
</div>
</div>
{service.display_detail_image ? ( {service.display_detail_image ? (
<div style={{ <div style={{
width: '100%', width: '100%',
background: '#111', background: '#111',
borderRadius: 12, borderRadius: 12,
overflow: 'hidden', overflow: 'hidden',
boxShadow: `0 0 30px ${service.color}22`, boxShadow: `0 0 30px ${service.color}22`,
border: `1px solid ${service.color}33` border: `1px solid ${service.color}33`
}}> }}>
<img <img
src={service.display_detail_image} src={service.display_detail_image}
alt={service.title} alt={service.title}
style={{ width: '100%', display: 'block' }} style={{ width: '100%', display: 'block' }}
/> />
</div> </div>
) : ( ) : (
<div style={{ textAlign: 'center', padding: 100, background: '#111', borderRadius: 12, color: '#666' }}> <div style={{ textAlign: 'center', padding: 100, background: '#111', borderRadius: 12, color: '#666' }}>
暂无详情图片 暂无详情图片
</div> </div>
)} )}
</Col>
<Col xs={24} md={8}>
<div style={{ position: 'sticky', top: 100 }}>
<div style={{
background: '#1f1f1f',
padding: 30,
borderRadius: 16,
border: `1px solid ${service.color}44`,
boxShadow: `0 0 20px ${service.color}11`
}}>
<Title level={3} style={{ color: '#fff', marginTop: 0 }}>服务报价</Title>
<div style={{ display: 'flex', alignItems: 'baseline', marginBottom: 20 }}>
<span style={{ fontSize: 36, color: service.color, fontWeight: 'bold' }}>¥{service.price}</span>
<span style={{ color: '#888', marginLeft: 8 }}>/ {service.unit} </span>
</div>
<div style={{ marginBottom: 20 }}>
{service.features_list && service.features_list.map((feat, i) => (
<Tag color={service.color} key={i} style={{ marginBottom: 8, padding: '4px 10px' }}>
{feat}
</Tag>
))}
</div>
<Button
type="primary"
size="large"
block
icon={<ShoppingCartOutlined />}
style={{
height: 50,
background: service.color,
borderColor: service.color,
color: '#000',
fontWeight: 'bold'
}}
onClick={() => setIsModalOpen(true)}
>
立即咨询 / 购买
</Button>
<p style={{ color: '#666', marginTop: 15, fontSize: 12, textAlign: 'center' }}>
* 具体价格可能因需求复杂度而异提交需求后我们将提供详细报价单
</p>
</div>
</div>
</Col>
</Row>
</motion.div> </motion.div>
{/* Purchase Modal */}
<Modal
title={`咨询/购买 - ${service.title}`}
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
footer={null}
destroyOnHidden
>
<p style={{ marginBottom: 20, color: '#666' }}>请填写您的联系方式和需求我们的技术顾问将在 24 小时内与您联系</p>
<Form
form={form}
layout="vertical"
onFinish={handlePurchase}
>
<Form.Item label="您的姓名" name="customer_name" rules={[{ required: true, message: '请输入姓名' }]}>
<Input placeholder="例如:张经理" />
</Form.Item>
<Form.Item label="公司/机构名称" name="company_name">
<Input placeholder="例如:某某科技有限公司" />
</Form.Item>
<Form.Item label="联系电话" name="phone_number" rules={[{ required: true, message: '请输入电话' }]}>
<Input placeholder="13800000000" />
</Form.Item>
<Form.Item label="电子邮箱" name="email" rules={[{ type: 'email' }]}>
<Input placeholder="example@company.com" />
</Form.Item>
<Form.Item label="需求描述" name="requirements">
<Input.TextArea rows={4} placeholder="请简单描述您的业务场景或具体需求..." />
</Form.Item>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 20 }}>
<Button onClick={() => setIsModalOpen(false)}>取消</Button>
<Button type="primary" htmlType="submit" loading={submitting}>提交需求</Button>
</div>
</Form>
</Modal>
</div> </div>
); );
}; };