diff --git a/backend/config/__pycache__/settings.cpython-312.pyc b/backend/config/__pycache__/settings.cpython-312.pyc index 0210904..86e1e32 100644 Binary files a/backend/config/__pycache__/settings.cpython-312.pyc and b/backend/config/__pycache__/settings.cpython-312.pyc differ diff --git a/backend/config/__pycache__/settings.cpython-313.pyc b/backend/config/__pycache__/settings.cpython-313.pyc index d702cc4..707d1a5 100644 Binary files a/backend/config/__pycache__/settings.cpython-313.pyc and b/backend/config/__pycache__/settings.cpython-313.pyc differ diff --git a/backend/config/__pycache__/urls.cpython-312.pyc b/backend/config/__pycache__/urls.cpython-312.pyc index 7669ebe..0a53fcf 100644 Binary files a/backend/config/__pycache__/urls.cpython-312.pyc and b/backend/config/__pycache__/urls.cpython-312.pyc differ diff --git a/backend/config/__pycache__/urls.cpython-313.pyc b/backend/config/__pycache__/urls.cpython-313.pyc index 0e17fa9..8d80a5b 100644 Binary files a/backend/config/__pycache__/urls.cpython-313.pyc and b/backend/config/__pycache__/urls.cpython-313.pyc differ diff --git a/backend/config/settings.py b/backend/config/settings.py index 35158e6..571e92e 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -32,6 +32,7 @@ ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ + 'unfold', # django-unfold必须在admin之前 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -40,6 +41,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'rest_framework', 'corsheaders', + 'drf_spectacular', # Swagger文档生成 'shop', ] @@ -126,3 +128,54 @@ USE_TZ = True # https://docs.djangoproject.com/en/6.0/howto/static-files/ 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)", + }, + }, +} diff --git a/backend/config/urls.py b/backend/config/urls.py index f3bc0eb..4f02810 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -1,7 +1,19 @@ from django.contrib import admin 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 = [ path('admin/', admin.site.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) diff --git a/backend/requirements.txt b/backend/requirements.txt index e3126a7..f6cc055 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,8 +1,18 @@ asgiref==3.11.0 +attrs==25.4.0 Django==6.0.1 django-cors-headers==4.9.0 +django-unfold==0.77.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 psycopg2-binary==2.9.11 +PyYAML==6.0.3 qrcode==8.2 +referencing==0.37.0 +rpds-py==0.30.0 sqlparse==0.5.5 +uritemplate==4.2.0 diff --git a/backend/shop/__pycache__/admin.cpython-312.pyc b/backend/shop/__pycache__/admin.cpython-312.pyc index 47f0e05..b359979 100644 Binary files a/backend/shop/__pycache__/admin.cpython-312.pyc and b/backend/shop/__pycache__/admin.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/admin.cpython-313.pyc b/backend/shop/__pycache__/admin.cpython-313.pyc index 3935e94..e8e6f5e 100644 Binary files a/backend/shop/__pycache__/admin.cpython-313.pyc and b/backend/shop/__pycache__/admin.cpython-313.pyc differ diff --git a/backend/shop/__pycache__/models.cpython-312.pyc b/backend/shop/__pycache__/models.cpython-312.pyc index 6b7e3df..9f9dae2 100644 Binary files a/backend/shop/__pycache__/models.cpython-312.pyc and b/backend/shop/__pycache__/models.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/models.cpython-313.pyc b/backend/shop/__pycache__/models.cpython-313.pyc index 6d22e88..f0d45e9 100644 Binary files a/backend/shop/__pycache__/models.cpython-313.pyc and b/backend/shop/__pycache__/models.cpython-313.pyc differ diff --git a/backend/shop/__pycache__/serializers.cpython-312.pyc b/backend/shop/__pycache__/serializers.cpython-312.pyc index 8de83ef..d22b019 100644 Binary files a/backend/shop/__pycache__/serializers.cpython-312.pyc and b/backend/shop/__pycache__/serializers.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/serializers.cpython-313.pyc b/backend/shop/__pycache__/serializers.cpython-313.pyc index 124ad0c..27c695d 100644 Binary files a/backend/shop/__pycache__/serializers.cpython-313.pyc and b/backend/shop/__pycache__/serializers.cpython-313.pyc differ diff --git a/backend/shop/__pycache__/urls.cpython-312.pyc b/backend/shop/__pycache__/urls.cpython-312.pyc index ad22381..72b89fc 100644 Binary files a/backend/shop/__pycache__/urls.cpython-312.pyc and b/backend/shop/__pycache__/urls.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/urls.cpython-313.pyc b/backend/shop/__pycache__/urls.cpython-313.pyc index 6fa21a8..d815fff 100644 Binary files a/backend/shop/__pycache__/urls.cpython-313.pyc and b/backend/shop/__pycache__/urls.cpython-313.pyc differ diff --git a/backend/shop/__pycache__/views.cpython-312.pyc b/backend/shop/__pycache__/views.cpython-312.pyc index 4b4d42f..0ba8b2b 100644 Binary files a/backend/shop/__pycache__/views.cpython-312.pyc and b/backend/shop/__pycache__/views.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/views.cpython-313.pyc b/backend/shop/__pycache__/views.cpython-313.pyc index 17b1a4f..54f0fe2 100644 Binary files a/backend/shop/__pycache__/views.cpython-313.pyc and b/backend/shop/__pycache__/views.cpython-313.pyc differ diff --git a/backend/shop/admin.py b/backend/shop/admin.py index 50e70b7..99780ed 100644 --- a/backend/shop/admin.py +++ b/backend/shop/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin from django.utils.html import format_html 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 import qrcode from io import BytesIO @@ -11,13 +13,13 @@ admin.site.site_header = "量迹AI硬件销售管理后台" admin.site.site_title = "量迹AI后台" admin.site.index_title = "欢迎使用量迹AI管理系统" -class ProductFeatureInline(admin.TabularInline): +class ProductFeatureInline(TabularInline): model = ProductFeature extra = 1 fields = ('title', 'description', 'icon_name', 'icon_image', 'icon_url', 'order') @admin.register(WeChatPayConfig) -class WeChatPayConfigAdmin(admin.ModelAdmin): +class WeChatPayConfigAdmin(ModelAdmin): list_display = ('app_id', 'mch_id', 'is_active', 'notify_url') list_filter = ('is_active',) search_fields = ('app_id', 'mch_id') @@ -34,7 +36,7 @@ class WeChatPayConfigAdmin(admin.ModelAdmin): ) @admin.register(ESP32Config) -class ESP32ConfigAdmin(admin.ModelAdmin): +class ESP32ConfigAdmin(ModelAdmin): list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone') list_filter = ('chip_type', 'has_camera') search_fields = ('name', 'description') @@ -53,13 +55,16 @@ class ESP32ConfigAdmin(admin.ModelAdmin): ) @admin.register(Service) -class ServiceAdmin(admin.ModelAdmin): +class ServiceAdmin(ModelAdmin): list_display = ('title', 'created_at') search_fields = ('title', 'description') fieldsets = ( ('基本信息', { 'fields': ('title', 'description', 'color') }), + ('价格与交付', { + 'fields': ('price', 'unit', 'delivery_time', 'delivery_content') + }), ('图标', { 'fields': ('icon', 'icon_url'), 'description': '图标上传和URL二选一,优先使用URL' @@ -74,7 +79,7 @@ class ServiceAdmin(admin.ModelAdmin): ) @admin.register(ARService) -class ARServiceAdmin(admin.ModelAdmin): +class ARServiceAdmin(ModelAdmin): list_display = ('title', 'created_at') search_fields = ('title', 'description') fieldsets = ( @@ -88,7 +93,7 @@ class ARServiceAdmin(admin.ModelAdmin): ) @admin.register(Salesperson) -class SalespersonAdmin(admin.ModelAdmin): +class SalespersonAdmin(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') @@ -100,12 +105,11 @@ class SalespersonAdmin(admin.ModelAdmin): ) return queryset + @display(description="累计销售额 (已支付)", ordering='_total_sales') 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) @@ -116,15 +120,16 @@ class SalespersonAdmin(admin.ModelAdmin): base_url = "http://localhost:5173" return f"{base_url}/?ref={obj.code}" + @display(description="推广链接") def view_promotion_url(self, obj): url = self.promotion_url(obj) - return format_html('打开推广链接', url) - view_promotion_url.short_description = "推广链接" + return format_html('打开推广链接', url) def promotion_url_display(self, obj): return self.promotion_url(obj) promotion_url_display.short_description = "完整推广链接" + @display(description="推广二维码") def promotion_qr_code(self, obj): if not obj.code: return "请先保存以生成二维码" @@ -144,9 +149,7 @@ class SalespersonAdmin(admin.ModelAdmin): img.save(buffer, format="PNG") img_str = base64.b64encode(buffer.getvalue()).decode() - return format_html('', img_str) - - promotion_qr_code.short_description = "推广二维码" + return format_html('', img_str) fieldsets = ( ('基本信息', { @@ -161,7 +164,7 @@ class SalespersonAdmin(admin.ModelAdmin): ) @admin.register(Order) -class OrderAdmin(admin.ModelAdmin): +class OrderAdmin(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') diff --git a/backend/shop/migrations/0008_service_delivery_content_service_delivery_time_and_more.py b/backend/shop/migrations/0008_service_delivery_content_service_delivery_time_and_more.py new file mode 100644 index 0000000..bf9889a --- /dev/null +++ b/backend/shop/migrations/0008_service_delivery_content_service_delivery_time_and_more.py @@ -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': '服务订单列表', + }, + ), + ] diff --git a/backend/shop/models.py b/backend/shop/models.py index 017467f..b7395d0 100644 --- a/backend/shop/models.py +++ b/backend/shop/models.py @@ -137,6 +137,10 @@ class Service(models.Model): icon_url = models.URLField(blank=True, null=True, verbose_name="图标 (URL)") description = models.TextField(verbose_name="简介") 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="主题色") 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)") @@ -150,6 +154,39 @@ class Service(models.Model): 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): """ AR体验服务模型 diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py index ce5ae6d..e7b5635 100644 --- a/backend/shop/serializers.py +++ b/backend/shop/serializers.py @@ -1,5 +1,5 @@ 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): """ @@ -49,6 +49,38 @@ class ServiceSerializer(serializers.ModelSerializer): return obj.detail_image.url 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): """ AR服务序列化器 diff --git a/backend/shop/urls.py b/backend/shop/urls.py index 40e6243..ebf41eb 100644 --- a/backend/shop/urls.py +++ b/backend/shop/urls.py @@ -1,12 +1,13 @@ from django.urls import path, include 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.register(r'configs', ESP32ConfigViewSet) router.register(r'orders', OrderViewSet) router.register(r'services', ServiceViewSet) router.register(r'ar', ARServiceViewSet) +router.register(r'service-orders', ServiceOrderViewSet) urlpatterns = [ path('', include(router.urls)), diff --git a/backend/shop/views.py b/backend/shop/views.py index 17e18f6..38b8606 100644 --- a/backend/shop/views.py +++ b/backend/shop/views.py @@ -2,8 +2,8 @@ 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 +from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService, ServiceOrder +from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer, ServiceOrderSerializer class ARServiceViewSet(viewsets.ReadOnlyModelViewSet): """ @@ -28,6 +28,13 @@ class ServiceViewSet(viewsets.ReadOnlyModelViewSet): queryset = Service.objects.all().order_by('-created_at') serializer_class = ServiceSerializer +class ServiceOrderViewSet(viewsets.ModelViewSet): + """ + AI服务订单管理 + """ + queryset = ServiceOrder.objects.all() + serializer_class = ServiceOrderSerializer + class ESP32ConfigViewSet(viewsets.ReadOnlyModelViewSet): """ 提供ESP32配置选项的列表和详情 diff --git a/frontend/src/api.js b/frontend/src/api.js index 8eb932e..29ea1d0 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -16,6 +16,7 @@ export const confirmPayment = (orderId) => api.post(`/orders/${orderId}/confirm_ export const getServices = () => api.get('/services/'); export const getServiceDetail = (id) => api.get(`/services/${id}/`); +export const createServiceOrder = (data) => api.post('/service-orders/', data); export const getARServices = () => api.get('/ar/'); export default api; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 3ae1e9c..71774e5 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -19,10 +19,9 @@ const Home = () => { fetchProducts(); let i = 0; const typingInterval = setInterval(() => { - if (i < fullText.length) { - setTypedText(prev => prev + fullText.charAt(i)); - i++; - } else { + i++; + setTypedText(fullText.slice(0, i)); + if (i >= fullText.length) { clearInterval(typingInterval); } }, 150); @@ -73,6 +72,26 @@ const Home = () => { return (
+ + + <span className="neon-text-green">{typedText}</span><span className="cursor-blink">|</span> diff --git a/frontend/src/pages/ServiceDetail.jsx b/frontend/src/pages/ServiceDetail.jsx index b393ccb..1630920 100644 --- a/frontend/src/pages/ServiceDetail.jsx +++ b/frontend/src/pages/ServiceDetail.jsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { Typography, Button, Spin, Empty } from 'antd'; -import { ArrowLeftOutlined } from '@ant-design/icons'; -import { getServiceDetail } from '../api'; +import { Typography, Button, Spin, Empty, Descriptions, Tag, Row, Col, Modal, Form, Input, message, Statistic } from 'antd'; +import { ArrowLeftOutlined, ClockCircleOutlined, GiftOutlined, ShoppingCartOutlined } from '@ant-design/icons'; +import { getServiceDetail, createServiceOrder } from '../api'; import { motion } from 'framer-motion'; const { Title, Paragraph } = Typography; @@ -12,6 +12,9 @@ const ServiceDetail = () => { const navigate = useNavigate(); const [service, setService] = useState(null); const [loading, setLoading] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [form] = Form.useForm(); useEffect(() => { const fetchDetail = async () => { @@ -27,6 +30,28 @@ const ServiceDetail = () => { fetchDetail(); }, [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) { return (
@@ -62,36 +87,135 @@ const ServiceDetail = () => { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} > -
- - {service.title} - - - {service.description} - -
+ + +
+ + {service.title} + + + {service.description} + + +
+ 服务详情 + + 交付周期}> + {service.delivery_time || '待沟通'} + + 交付内容}> + {service.delivery_content || '根据需求定制'} + + +
+
- {service.display_detail_image ? ( -
- {service.title} -
- ) : ( -
- 暂无详情图片 -
- )} + {service.display_detail_image ? ( +
+ {service.title} +
+ ) : ( +
+ 暂无详情图片 +
+ )} + + + +
+
+ 服务报价 +
+ ¥{service.price} + / {service.unit} 起 +
+ +
+ {service.features_list && service.features_list.map((feat, i) => ( + + {feat} + + ))} +
+ + +

+ * 具体价格可能因需求复杂度而异,提交需求后我们将提供详细报价单 +

+
+
+ +
+ + {/* Purchase Modal */} + setIsModalOpen(false)} + footer={null} + destroyOnHidden + > +

请填写您的联系方式和需求,我们的技术顾问将在 24 小时内与您联系。

+
+ + + + + + + + + + + + + + + + +
+ + +
+
+
); };