vb
@@ -12,7 +12,7 @@ links = [
|
||||
"admin:shop_distributor_changelist",
|
||||
"admin:shop_esp32config_changelist",
|
||||
"admin:shop_service_changelist",
|
||||
"admin:shop_arservice_changelist",
|
||||
"admin:shop_vbcourse_changelist",
|
||||
"admin:shop_order_changelist",
|
||||
"admin:shop_serviceorder_changelist",
|
||||
"admin:shop_withdrawal_changelist",
|
||||
|
||||
@@ -232,9 +232,9 @@ UNFOLD = {
|
||||
"link": reverse_lazy("admin:shop_service_changelist"),
|
||||
},
|
||||
{
|
||||
"title": "AR体验",
|
||||
"icon": "view_in_ar",
|
||||
"link": reverse_lazy("admin:shop_arservice_changelist"),
|
||||
"title": "VB课程",
|
||||
"icon": "school",
|
||||
"link": reverse_lazy("admin:shop_vbcourse_changelist"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.db.models import Sum
|
||||
from django import forms
|
||||
from unfold.admin import ModelAdmin, TabularInline
|
||||
from unfold.decorators import display
|
||||
from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder
|
||||
from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VBCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder
|
||||
import qrcode
|
||||
from io import BytesIO
|
||||
import base64
|
||||
@@ -19,11 +19,11 @@ class ExternalUploadWidget(forms.URLInput):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.upload_url = upload_url
|
||||
self.attrs.update({
|
||||
'class': 'upload-url-input',
|
||||
'class': 'upload-url-input vTextField',
|
||||
'data-upload-url': upload_url,
|
||||
'data-accept': accept,
|
||||
'readonly': 'readonly',
|
||||
'placeholder': '上传文件后自动生成URL'
|
||||
'placeholder': '上传文件后自动生成URL',
|
||||
'style': 'width: 100%;'
|
||||
})
|
||||
|
||||
class Media:
|
||||
@@ -141,18 +141,26 @@ class ServiceOrderAdmin(ModelAdmin):
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(ARService)
|
||||
class ARServiceAdmin(ModelAdmin):
|
||||
list_display = ('title', 'created_at')
|
||||
search_fields = ('title', 'description')
|
||||
@admin.register(VBCourse)
|
||||
class VBCourseAdmin(ModelAdmin):
|
||||
list_display = ('title', 'course_type', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at')
|
||||
search_fields = ('title', 'description', 'instructor', 'tag')
|
||||
list_filter = ('course_type', 'instructor', 'tag')
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('title', 'description')
|
||||
'fields': ('title', 'description', 'course_type', 'tag')
|
||||
}),
|
||||
('封面/长图', {
|
||||
('课程详情', {
|
||||
'fields': ('instructor', 'duration', 'lesson_count')
|
||||
}),
|
||||
('封面', {
|
||||
'fields': ('cover_image', 'cover_image_url'),
|
||||
'description': '图片上传和URL二选一,优先使用URL'
|
||||
}),
|
||||
('详情页长图', {
|
||||
'fields': ('detail_image', 'detail_image_url'),
|
||||
'description': '图片上传和URL二选一,优先使用URL'
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(Salesperson)
|
||||
|
||||
35
backend/shop/migrations/0018_vbcourse_delete_arservice.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-10 18:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0017_withdrawal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VBCourse',
|
||||
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='课程简介')),
|
||||
('course_type', models.CharField(choices=[('software', '软件课程'), ('hardware', '硬件课程')], default='software', max_length=20, verbose_name='课程类型')),
|
||||
('duration', models.CharField(default='30分钟', help_text='例如: 30分钟', max_length=50, verbose_name='课程时长')),
|
||||
('lesson_count', models.IntegerField(default=1, verbose_name='课时数量')),
|
||||
('instructor', models.CharField(default='VB讲师', max_length=50, verbose_name='讲师')),
|
||||
('cover_image', models.ImageField(blank=True, null=True, upload_to='courses/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': 'VB课程',
|
||||
'verbose_name_plural': 'VB课程管理',
|
||||
},
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='ARService',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-10 18:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0018_vbcourse_delete_arservice'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='vbcourse',
|
||||
name='detail_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='courses/details/', verbose_name='详情页长图 (上传)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vbcourse',
|
||||
name='detail_image_url',
|
||||
field=models.URLField(blank=True, help_text='如果填写了URL,将优先使用URL', null=True, verbose_name='详情页长图 (URL)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vbcourse',
|
||||
name='tag',
|
||||
field=models.CharField(blank=True, help_text='例如: 热门, 推荐, 进阶', max_length=20, verbose_name='标签'),
|
||||
),
|
||||
]
|
||||
18
backend/shop/migrations/0020_alter_vbcourse_course_type.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-10 18:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0019_vbcourse_detail_image_vbcourse_detail_image_url_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='vbcourse',
|
||||
name='course_type',
|
||||
field=models.CharField(choices=[('software', '软件课程'), ('hardware', '硬件课程'), ('incubation', '产品商业孵化')], default='software', max_length=20, verbose_name='课程类型'),
|
||||
),
|
||||
]
|
||||
@@ -312,19 +312,36 @@ class ServiceOrder(models.Model):
|
||||
verbose_name_plural = "服务订单列表"
|
||||
|
||||
|
||||
class ARService(models.Model):
|
||||
class VBCourse(models.Model):
|
||||
"""
|
||||
AR体验服务模型
|
||||
VB Coding 课程模型
|
||||
"""
|
||||
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)")
|
||||
COURSE_TYPE_CHOICES = (
|
||||
('software', '软件课程'),
|
||||
('hardware', '硬件课程'),
|
||||
('incubation', '产品商业孵化'),
|
||||
)
|
||||
|
||||
title = models.CharField(max_length=100, verbose_name="课程名称")
|
||||
description = models.TextField(verbose_name="课程简介")
|
||||
course_type = models.CharField(max_length=20, choices=COURSE_TYPE_CHOICES, default='software', verbose_name="课程类型")
|
||||
duration = models.CharField(max_length=50, verbose_name="课程时长", help_text="例如: 30分钟", default="30分钟")
|
||||
lesson_count = models.IntegerField(default=1, verbose_name="课时数量")
|
||||
instructor = models.CharField(max_length=50, verbose_name="讲师", default="VB讲师")
|
||||
|
||||
tag = models.CharField(max_length=20, blank=True, verbose_name="标签", help_text="例如: 热门, 推荐, 进阶")
|
||||
|
||||
cover_image = models.ImageField(upload_to='courses/covers/', blank=True, null=True, verbose_name="封面图 (上传)")
|
||||
cover_image_url = models.URLField(blank=True, null=True, verbose_name="封面图 (URL)")
|
||||
|
||||
detail_image = models.ImageField(upload_to='courses/details/', blank=True, null=True, verbose_name="详情页长图 (上传)")
|
||||
detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)", help_text="如果填写了URL,将优先使用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体验管理"
|
||||
verbose_name = "VB课程"
|
||||
verbose_name_plural = "VB课程管理"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from rest_framework import serializers
|
||||
from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal
|
||||
from .models import ESP32Config, Order, Salesperson, Service, VBCourse, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal
|
||||
|
||||
class WeChatUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
@@ -101,14 +101,16 @@ class ServiceOrderSerializer(serializers.ModelSerializer):
|
||||
|
||||
return super().create(validated_data)
|
||||
|
||||
class ARServiceSerializer(serializers.ModelSerializer):
|
||||
class VBCourseSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
AR服务序列化器
|
||||
VB课程序列化器
|
||||
"""
|
||||
display_cover_image = serializers.SerializerMethodField()
|
||||
display_detail_image = serializers.SerializerMethodField()
|
||||
course_type_display = serializers.CharField(source='get_course_type_display', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ARService
|
||||
model = VBCourse
|
||||
fields = '__all__'
|
||||
|
||||
def get_display_cover_image(self, obj):
|
||||
@@ -118,6 +120,13 @@ class ARServiceSerializer(serializers.ModelSerializer):
|
||||
return obj.cover_image.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 ESP32ConfigSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
ESP32配置序列化器
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.urls import path, include, re_path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import (
|
||||
ESP32ConfigViewSet, OrderViewSet, order_check_view,
|
||||
ServiceViewSet, ARServiceViewSet, ServiceOrderViewSet,
|
||||
ServiceViewSet, VBCourseViewSet, ServiceOrderViewSet,
|
||||
payment_finish, pay, send_sms_code, wechat_login, update_user_info, DistributorViewSet
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ 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'courses', VBCourseViewSet)
|
||||
router.register(r'service-orders', ServiceOrderViewSet)
|
||||
router.register(r'distributor', DistributorViewSet, basename='distributor')
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ from django.shortcuts import render
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.http import HttpResponse
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiExample
|
||||
from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService, ServiceOrder, Salesperson, CommissionLog, WeChatUser, Distributor, Withdrawal
|
||||
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer
|
||||
from .models import ESP32Config, Order, WeChatPayConfig, Service, VBCourse, ServiceOrder, Salesperson, CommissionLog, WeChatUser, Distributor, Withdrawal
|
||||
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, VBCourseSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer
|
||||
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
|
||||
from django.contrib.auth.models import User
|
||||
from wechatpayv3 import WeChatPay, WeChatPayType
|
||||
@@ -508,15 +508,15 @@ def payment_finish(request):
|
||||
return HttpResponse(str(e), status=500)
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(summary="获取AR服务列表", description="获取所有可用的AR服务"),
|
||||
retrieve=extend_schema(summary="获取AR服务详情", description="获取指定AR服务的详细信息")
|
||||
list=extend_schema(summary="获取VB课程列表", description="获取所有可用的VB课程"),
|
||||
retrieve=extend_schema(summary="获取VB课程详情", description="获取指定VB课程的详细信息")
|
||||
)
|
||||
class ARServiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class VBCourseViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
AR服务列表和详情
|
||||
VB课程列表和详情
|
||||
"""
|
||||
queryset = ARService.objects.all().order_by('-created_at')
|
||||
serializer_class = ARServiceSerializer
|
||||
queryset = VBCourse.objects.all().order_by('-created_at')
|
||||
serializer_class = VBCourseSerializer
|
||||
|
||||
def order_check_view(request):
|
||||
"""
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 VBCourses from './pages/VBCourses';
|
||||
import MyOrders from './pages/MyOrders';
|
||||
import 'antd/dist/reset.css';
|
||||
import './App.css';
|
||||
@@ -19,7 +19,7 @@ function App() {
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/services" element={<AIServices />} />
|
||||
<Route path="/services/:id" element={<ServiceDetail />} />
|
||||
<Route path="/ar" element={<ARExperience />} />
|
||||
<Route path="/courses" element={<VBCourses />} />
|
||||
<Route path="/my-orders" element={<MyOrders />} />
|
||||
<Route path="/product/:id" element={<ProductDetail />} />
|
||||
<Route path="/payment/:orderId" element={<Payment />} />
|
||||
|
||||
@@ -19,7 +19,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 const getVBCourses = () => api.get('/courses/');
|
||||
|
||||
export const sendSms = (data) => api.post('/auth/send-sms/', data);
|
||||
export const queryMyOrders = (data) => api.post('/orders/my_orders/', data);
|
||||
|
||||
@@ -34,9 +34,9 @@ const Layout = ({ children }) => {
|
||||
label: 'AI 服务',
|
||||
},
|
||||
{
|
||||
key: '/ar',
|
||||
key: '/courses',
|
||||
icon: <EyeOutlined />,
|
||||
label: 'AR 体验',
|
||||
label: 'VB 课程',
|
||||
},
|
||||
{
|
||||
key: '/my-orders',
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
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';
|
||||
import { Button, Typography, Spin, Row, Col, Empty, Tag } from 'antd';
|
||||
import { ReadOutlined, ClockCircleOutlined, UserOutlined, BookOutlined } from '@ant-design/icons';
|
||||
import { getVBCourses } from '../api';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const ARExperience = () => {
|
||||
const [scanning, setScanning] = useState(true);
|
||||
const [arServices, setArServices] = useState([]);
|
||||
const VBCourses = () => {
|
||||
const [courses, setCourses] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAR = async () => {
|
||||
const fetchCourses = async () => {
|
||||
try {
|
||||
const res = await getARServices();
|
||||
setArServices(res.data);
|
||||
const res = await getVBCourses();
|
||||
setCourses(res.data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch AR services:", error);
|
||||
console.error("Failed to fetch VB Courses:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
fetchAR();
|
||||
fetchCourses();
|
||||
}, []);
|
||||
|
||||
if (loading) return <div style={{ textAlign: 'center', padding: 100 }}><Spin size="large" /></div>;
|
||||
@@ -31,20 +30,20 @@ const ARExperience = () => {
|
||||
<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>
|
||||
VB <span style={{ color: '#00f0ff' }}>CODING COURSES</span>
|
||||
</Title>
|
||||
<Paragraph style={{ color: '#aaa', fontSize: 18, maxWidth: 600, margin: '0 auto' }}>
|
||||
探索全息增强现实体验。请佩戴您的设备,或使用移动端摄像头扫描空间。
|
||||
探索 Vibe Coding 软件与硬件课程,开启您的编程之旅。
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{arServices.length === 0 ? (
|
||||
{courses.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', marginTop: 100, zIndex: 2, position: 'relative' }}>
|
||||
<Empty description={<span style={{ color: '#666' }}>暂无 AR 体验内容</span>} />
|
||||
<Empty description={<span style={{ color: '#666' }}>暂无课程内容</span>} />
|
||||
</div>
|
||||
) : (
|
||||
<Row gutter={[32, 32]} justify="center" style={{ padding: '0 20px', position: 'relative', zIndex: 2 }}>
|
||||
{arServices.map((item, index) => (
|
||||
{courses.map((item, index) => (
|
||||
<Col xs={24} md={12} lg={8} key={item.id}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
@@ -57,20 +56,35 @@ const ARExperience = () => {
|
||||
border: '1px solid rgba(0,240,255,0.2)',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
height: '100%'
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
<div style={{ height: 200, background: '#000', overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ height: 200, background: '#000', overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
|
||||
{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' }} />
|
||||
<ReadOutlined style={{ fontSize: 40, color: '#333' }} />
|
||||
)}
|
||||
<div style={{ position: 'absolute', top: 10, right: 10, display: 'flex', gap: '5px' }}>
|
||||
{item.tag && (
|
||||
<Tag color="volcano" style={{ marginRight: 0 }}>{item.tag}</Tag>
|
||||
)}
|
||||
<Tag color={item.course_type === 'hardware' ? 'purple' : 'cyan'}>
|
||||
{item.course_type_display || (item.course_type === 'hardware' ? '硬件课程' : '软件课程')}
|
||||
</Tag>
|
||||
</div>
|
||||
</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>
|
||||
<div style={{ padding: 20, flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<h3 style={{ color: '#fff', fontSize: 20, marginBottom: 10 }}>{item.title}</h3>
|
||||
<div style={{ color: '#888', marginBottom: 15, fontSize: 14 }}>
|
||||
<span style={{ marginRight: 15 }}><UserOutlined /> {item.instructor}</span>
|
||||
<span style={{ marginRight: 15 }}><ClockCircleOutlined /> {item.duration}</span>
|
||||
<span><BookOutlined /> {item.lesson_count} 课时</span>
|
||||
</div>
|
||||
<p style={{ color: '#aaa', marginBottom: 20, flex: 1 }}>{item.description}</p>
|
||||
<Button type="primary" block ghost style={{ borderColor: '#00f0ff', color: '#00f0ff' }}>
|
||||
启动体验
|
||||
开始学习
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,4 +122,4 @@ const ARExperience = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ARExperience;
|
||||
export default VBCourses;
|
||||
@@ -22,7 +22,7 @@ const config = {
|
||||
framework: 'react',
|
||||
compiler: 'webpack5',
|
||||
cache: {
|
||||
enable: false // Disable cache to avoid potential issues
|
||||
enable: true // Enable cache for better build performance
|
||||
},
|
||||
mini: {
|
||||
postcss: {
|
||||
|
||||
@@ -15,15 +15,18 @@ export const getServices = () => request({ url: '/services/' })
|
||||
export const getServiceDetail = (id: number) => request({ url: `/services/${id}/` })
|
||||
export const createServiceOrder = (data: any) => request({ url: '/service-orders/', method: 'POST', data })
|
||||
|
||||
// AR Services
|
||||
export const getARServices = () => request({ url: '/ar/' })
|
||||
export const getARServiceDetail = (id: number) => request({ url: `/ar/${id}/` })
|
||||
// VB Courses
|
||||
export const getVBCourses = () => request({ url: '/courses/' })
|
||||
export const getVBCourseDetail = (id: number) => request({ url: `/courses/${id}/` })
|
||||
|
||||
// Distributor
|
||||
export const distributorRegister = (data: any) => request({ url: '/distributor/register/', method: 'POST', data })
|
||||
export const distributorInfo = () => request({ url: '/distributor/info/' })
|
||||
export const distributorInvite = () => request({ url: '/distributor/invite/', method: 'POST' })
|
||||
export const distributorWithdraw = (amount: number) => request({ url: '/distributor/withdraw/', method: 'POST', data: { amount } })
|
||||
// TODO: Verify if these exist in the API docs
|
||||
// export const distributorTeam = () => request({ url: '/distributor/team/' })
|
||||
// export const distributorHistory = () => request({ url: '/distributor/history/' })
|
||||
|
||||
// User
|
||||
export const updateUserInfo = (data: any) => request({ url: '/wechat/update/', method: 'POST', data })
|
||||
|
||||
@@ -3,8 +3,8 @@ export default defineAppConfig({
|
||||
'pages/index/index',
|
||||
'pages/services/index',
|
||||
'pages/services/detail',
|
||||
'pages/ar/index',
|
||||
'pages/ar/detail',
|
||||
'pages/courses/index',
|
||||
'pages/courses/detail',
|
||||
'pages/goods/detail',
|
||||
'pages/cart/cart',
|
||||
'pages/order/checkout',
|
||||
@@ -25,14 +25,15 @@ export default defineAppConfig({
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarBackgroundColor: '#000000',
|
||||
navigationBarTitleText: 'Quant Speed Market',
|
||||
navigationBarTextStyle: 'black'
|
||||
navigationBarTextStyle: 'white'
|
||||
},
|
||||
tabBar: {
|
||||
color: "#999",
|
||||
selectedColor: "#333",
|
||||
backgroundColor: "#fff",
|
||||
color: "#666666",
|
||||
selectedColor: "#00b96b",
|
||||
backgroundColor: "#000000",
|
||||
borderStyle: "black",
|
||||
list: [
|
||||
{
|
||||
pagePath: "pages/index/index",
|
||||
@@ -43,13 +44,19 @@ export default defineAppConfig({
|
||||
{
|
||||
pagePath: "pages/services/index",
|
||||
text: "AI服务",
|
||||
iconPath: "./assets/cart.png", // Using cart icon as placeholder if no other icon available
|
||||
selectedIconPath: "./assets/cart_active.png"
|
||||
iconPath: "./assets/AI_service.png",
|
||||
selectedIconPath: "./assets/AI_service_active.png"
|
||||
},
|
||||
{
|
||||
pagePath: "pages/ar/index",
|
||||
text: "AR体验",
|
||||
iconPath: "./assets/cart.png", // Placeholder
|
||||
pagePath: "pages/courses/index",
|
||||
text: "VB课程",
|
||||
iconPath: "./assets/VR.png",
|
||||
selectedIconPath: "./assets/VR_active.png"
|
||||
},
|
||||
{
|
||||
pagePath: "pages/cart/cart",
|
||||
text: "购物车",
|
||||
iconPath: "./assets/cart.png",
|
||||
selectedIconPath: "./assets/cart_active.png"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
page {
|
||||
background-color: #f7f8fa;
|
||||
--primary-cyan: #00f0ff;
|
||||
--primary-green: #00b96b;
|
||||
--primary-purple: #bd00ff;
|
||||
--bg-dark: #050505;
|
||||
--card-bg: rgba(255, 255, 255, 0.03);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--text-main: #ffffff;
|
||||
--text-secondary: rgba(255, 255, 255, 0.7);
|
||||
|
||||
background-color: var(--bg-dark);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica,
|
||||
Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei',
|
||||
sans-serif;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.container {
|
||||
|
||||
BIN
miniprogram/src/assets/AI_service.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
miniprogram/src/assets/AI_service_active.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
miniprogram/src/assets/VR.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
miniprogram/src/assets/VR_active.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 140 B After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 79 B After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 140 B After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 79 B After Width: | Height: | Size: 10 KiB |
@@ -1,6 +1,104 @@
|
||||
<svg width="66" height="66" viewBox="0 0 66 66" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M49.4996 16.5C49.4996 7.38729 42.1123 0 32.9996 0C23.8869 0 16.4996 7.38729 16.4996 16.5H49.4996Z" fill="white"/>
|
||||
<path d="M33 66C42.1127 66 49.5 58.6127 49.5 49.5H16.5C16.5 58.6127 23.8873 66 33 66Z" fill="white"/>
|
||||
<path d="M66 33C66 42.1127 58.6127 49.5 49.5 49.5V16.5C58.6127 16.5 66 23.8873 66 33Z" fill="white"/>
|
||||
<path d="M0 33C0 23.8873 7.3873 16.5 16.5 16.5V49.5C7.3873 49.5 0 42.1127 0 33Z" fill="white"/>
|
||||
<?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.050000,-0.050000)"
|
||||
fill="#FFFFFF" stroke="none">
|
||||
<path d="M1836 3803 c-149 -150 -196 -223 -196 -304 0 -58 24 -46 85 42 95
|
||||
137 365 284 404 220 17 -27 127 -410 120 -417 -3 -4 -43 21 -90 55 -108 78
|
||||
-289 58 -349 -38 -27 -45 9 -46 101 -2 225 105 249 -11 66 -313 -41 -68 -99
|
||||
-170 -127 -227 -47 -91 -58 -101 -102 -90 -78 19 -88 -3 -48 -95 21 -47 44
|
||||
-125 52 -172 29 -184 162 -172 247 23 25 58 98 213 161 345 63 132 121 267
|
||||
129 300 18 75 31 60 153 -178 208 -401 479 -647 755 -684 106 -14 223 32 223
|
||||
87 0 64 -209 203 -307 205 -115 1 -389 222 -556 447 -182 243 -255 595 -117
|
||||
559 49 -13 68 30 43 96 -28 71 -174 218 -217 218 -20 0 -86 18 -146 40 -60 22
|
||||
-114 40 -120 39 -5 0 -79 -70 -164 -156z"/>
|
||||
<path d="M5153 3827 c-24 -25 -13 -47 23 -47 98 0 112 -112 22 -178 -50 -37
|
||||
-78 -44 -140 -34 -100 16 -109 -43 -18 -108 79 -56 76 -68 -29 -108 -115 -45
|
||||
-191 -105 -191 -153 0 -45 18 -48 87 -17 59 27 142 -9 125 -54 -6 -15 -3 -50
|
||||
7 -75 13 -35 9 -56 -16 -82 -31 -32 -29 -34 27 -28 118 11 61 -34 -136 -108
|
||||
-121 -45 -234 -75 -288 -75 -104 0 -107 -30 -8 -87 l64 -38 177 83 c182 84
|
||||
241 99 241 60 0 -53 -93 -181 -166 -229 -100 -67 -76 -108 43 -72 126 38 174
|
||||
89 224 242 39 117 54 141 97 150 103 21 219 18 248 -6 59 -49 423 -22 430 32
|
||||
6 37 -8 52 -70 76 -42 17 -98 41 -126 53 -27 13 -59 21 -70 18 -29 -8 -357
|
||||
-61 -376 -62 -67 -1 7 94 95 123 145 48 141 75 -17 126 -35 11 -34 18 23 77
|
||||
47 48 58 72 44 96 -22 40 30 93 114 116 83 22 78 60 -21 167 -70 76 -101 95
|
||||
-155 96 -37 0 -89 13 -117 29 -53 30 -125 39 -147 17z m286 -115 c27 -33 -11
|
||||
-144 -66 -189 -43 -36 -44 -1 -5 113 16 44 24 86 17 92 -6 7 -2 12 9 12 12 0
|
||||
32 -13 45 -28z m-211 -378 c17 -27 10 -45 -33 -88 l-55 -55 -70 38 c-89 47
|
||||
-88 54 15 106 95 47 113 47 143 -1z"/>
|
||||
<path d="M1151 3392 c11 -60 8 -72 -21 -72 -19 0 -52 -22 -74 -50 -21 -27 -53
|
||||
-50 -70 -50 -25 0 -26 5 -6 30 14 17 20 36 14 43 -7 7 -23 -14 -35 -45 -41
|
||||
-103 -53 -118 -94 -118 -59 0 -75 -24 -27 -40 23 -7 43 -23 44 -36 6 -59 -8
|
||||
-127 -31 -154 -41 -50 -62 -95 -30 -65 38 35 59 31 58 -10 0 -42 -96 -117
|
||||
-118 -92 -109 123 -96 467 17 467 12 0 22 9 22 20 0 11 -12 20 -26 20 -14 0
|
||||
-61 18 -105 40 -110 57 -138 41 -99 -53 17 -41 44 -147 59 -236 16 -88 43
|
||||
-194 60 -235 43 -103 40 -114 -37 -146 -66 -28 -92 -63 -92 -125 0 -26 20 -29
|
||||
128 -19 100 9 175 1 332 -37 281 -68 620 -39 620 53 0 44 -63 76 -112 57 -28
|
||||
-10 -98 -19 -157 -19 l-106 0 -12 75 c-7 41 -13 130 -13 197 l0 123 85 31 c47
|
||||
17 108 34 135 38 28 3 50 19 50 36 0 41 -124 79 -215 66 -85 -13 -115 59 -35
|
||||
84 49 16 53 81 6 107 -19 11 -44 53 -55 94 -29 103 -79 120 -60 21z m9 -367
|
||||
c0 -93 -110 -178 -146 -113 -17 31 -22 29 -46 -14 l-27 -48 0 54 c-1 34 12 57
|
||||
35 65 21 6 63 38 94 71 75 78 90 76 90 -15z m0 -254 c0 -105 -24 -126 -91 -82
|
||||
-74 48 -59 13 35 -83 106 -108 100 -113 -121 -83 -178 23 -183 26 -183 76 0
|
||||
60 95 129 148 109 21 -8 37 2 48 31 14 38 111 116 149 120 8 0 15 -39 15 -88z"/>
|
||||
<path d="M3842 3282 c-44 -28 -6 -62 70 -62 96 0 201 -48 218 -99 22 -69 -171
|
||||
-359 -400 -601 -253 -267 -269 -338 -114 -505 71 -76 113 -69 184 30 34 47
|
||||
107 147 163 223 309 424 418 676 349 808 -75 145 -365 272 -470 206z"/>
|
||||
<path d="M5331 2779 c7 -12 75 -62 151 -110 84 -54 138 -103 138 -124 1 -68
|
||||
198 -200 226 -150 55 98 13 173 -156 281 -125 79 -392 156 -359 103z"/>
|
||||
<path d="M4240 2461 c0 -10 52 -59 116 -108 64 -49 147 -124 186 -166 170
|
||||
-188 257 -247 361 -247 206 0 55 221 -243 355 -60 27 -173 80 -250 117 -148
|
||||
73 -170 79 -170 49z"/>
|
||||
<path d="M1145 2247 c-31 -13 -277 -299 -335 -392 -20 -31 -151 -222 -327
|
||||
-475 -49 -71 -106 -148 -126 -170 -19 -23 -59 -86 -88 -140 -29 -55 -92 -151
|
||||
-140 -213 -129 -165 -137 -196 -89 -356 83 -281 158 -274 309 29 63 127 149
|
||||
268 192 315 42 47 87 109 99 137 21 49 34 53 186 65 90 7 223 18 295 25 118
|
||||
12 130 9 131 -25 0 -20 7 -71 15 -114 14 -67 10 -77 -27 -87 -85 -22 67 -396
|
||||
161 -396 l49 0 -1 240 c-1 340 -88 1041 -148 1198 -13 35 -17 104 -9 172 19
|
||||
167 -33 233 -147 187z m44 -412 c16 -104 51 -542 43 -550 -4 -4 -82 -10 -175
|
||||
-14 -92 -3 -187 -13 -212 -21 -67 -22 -57 16 43 178 48 78 94 164 102 192 19
|
||||
61 155 280 174 280 8 0 19 -29 25 -65z"/>
|
||||
<path d="M5212 1983 c-7 -11 15 -118 49 -237 161 -559 176 -476 -116 -627 -94
|
||||
-49 -189 -113 -211 -143 -22 -30 -48 -50 -57 -44 -36 22 -14 79 65 173 266
|
||||
315 280 517 46 634 -133 66 -203 42 -106 -37 65 -52 -40 -220 -319 -507 l-111
|
||||
-114 34 -71 c19 -39 34 -83 34 -98 0 -60 78 -102 145 -76 80 30 80 29 52 -94
|
||||
-15 -65 -17 -110 -6 -118 11 -8 37 -50 59 -94 82 -164 121 -122 113 120 -8
|
||||
250 11 297 131 317 42 7 147 44 235 83 87 38 162 65 167 61 10 -10 79 -260
|
||||
113 -411 14 -60 40 -145 58 -187 18 -43 33 -98 33 -122 0 -63 140 -310 201
|
||||
-355 126 -93 150 -17 146 464 -5 576 -6 600 -27 600 -11 0 -20 -27 -20 -60 0
|
||||
-71 -107 -415 -146 -468 -25 -34 -30 -29 -54 60 -14 54 -40 125 -57 158 -38
|
||||
74 -122 328 -123 372 0 17 25 44 55 59 30 16 80 46 110 67 30 22 93 44 140 51
|
||||
181 24 102 101 -155 150 -262 50 -273 59 -338 267 -54 174 -114 270 -140 227z
|
||||
m-364 -559 c-75 -99 -310 -344 -331 -344 -9 0 71 103 178 228 180 211 282 288
|
||||
153 116z m691 56 c102 -52 111 -69 52 -108 -75 -49 -103 -39 -128 45 -36 122
|
||||
-37 121 76 63z"/>
|
||||
<path d="M5560 1980 c0 -11 9 -20 19 -20 11 0 27 -29 37 -65 11 -46 36 -75 85
|
||||
-98 131 -63 148 35 26 143 -66 58 -167 82 -167 40z"/>
|
||||
<path d="M2490 1947 c-69 -16 -169 -24 -235 -18 -100 9 -115 6 -115 -24 1 -77
|
||||
77 -125 201 -125 71 0 81 -5 69 -35 -8 -19 -19 -53 -25 -75 -23 -95 -197 -554
|
||||
-265 -700 -99 -214 -106 -219 -298 -203 -170 15 -182 8 -182 -96 0 -89 28 -94
|
||||
506 -84 446 9 534 23 534 85 0 66 -265 168 -337 129 -161 -86 -171 29 -20 242
|
||||
20 29 37 71 37 93 0 44 72 216 166 394 39 75 63 150 63 200 l1 80 145 1 c165
|
||||
1 212 26 158 85 -72 80 -205 97 -403 51z"/>
|
||||
<path d="M3873 1627 c-7 -84 -13 -191 -13 -237 l0 -84 -135 -51 c-74 -28 -160
|
||||
-57 -191 -64 -32 -7 -53 -23 -49 -37 14 -42 167 -39 267 5 51 22 96 41 101 41
|
||||
4 0 7 -60 7 -132 l0 -133 -65 15 c-148 35 -275 -7 -275 -92 0 -32 5 -33 61 -4
|
||||
62 31 169 24 244 -16 37 -21 48 -149 22 -277 -20 -100 -244 -43 -288 73 -11
|
||||
28 -25 47 -30 42 -40 -41 318 -436 394 -436 65 0 77 88 77 569 l0 467 63 22
|
||||
c35 12 89 22 120 22 94 0 77 73 -23 100 -99 27 -160 90 -160 164 0 214 -109
|
||||
251 -127 43z"/>
|
||||
<path d="M3063 1349 c-22 -28 -53 -94 -69 -145 -85 -278 -83 -254 -38 -350 77
|
||||
-164 174 -184 245 -51 23 41 24 41 80 -21 95 -104 146 -23 147 238 2 292 -70
|
||||
418 -194 341 -33 -21 -43 -20 -58 7 -26 46 -67 39 -113 -19z m237 -129 c0 -59
|
||||
-8 -80 -31 -80 -17 0 -58 -14 -90 -31 -82 -42 -81 8 3 113 81 103 118 102 118
|
||||
-2z m0 -167 c0 -70 -82 -137 -122 -101 -53 48 -47 81 17 105 78 28 105 27 105
|
||||
-4z m-140 -132 c16 -32 13 -43 -20 -61 -36 -19 -40 -15 -40 39 0 69 29 80 60
|
||||
22z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 526 B After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 140 B After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 79 B After Width: | Height: | Size: 3.5 KiB |
@@ -1,51 +0,0 @@
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import Taro, { useLoad } from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import { getARServiceDetail } from '../../api'
|
||||
import './detail.scss'
|
||||
|
||||
export default function ARDetail() {
|
||||
const [detail, setDetail] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useLoad((options) => {
|
||||
if (options.id) fetchDetail(options.id)
|
||||
})
|
||||
|
||||
const fetchDetail = async (id: string) => {
|
||||
try {
|
||||
const res: any = await getARServiceDetail(Number(id))
|
||||
setDetail(res)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLaunch = () => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '请使用摄像头扫描空间以启动 AR 体验 (演示模式)',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
if (loading) return <View className='page-container'><Text style={{color:'#fff'}}>Loading...</Text></View>
|
||||
if (!detail) return <View className='page-container'><Text style={{color:'#fff'}}>Not Found</Text></View>
|
||||
|
||||
return (
|
||||
<View className='page-container'>
|
||||
<Text className='title'>{detail.title}</Text>
|
||||
<Text className='desc'>{detail.description}</Text>
|
||||
|
||||
<View className='ar-placeholder'>
|
||||
<Text className='icon'>📷</Text>
|
||||
<Text className='text'>AR 场景加载区域</Text>
|
||||
</View>
|
||||
|
||||
<Button className='btn-launch' onClick={handleLaunch}>进入沉浸模式</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,214 @@
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f7f8fa;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background-color: #050505;
|
||||
color: #fff;
|
||||
padding-bottom: 120px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-list {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.cart-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.checkbox-area {
|
||||
padding: 10px;
|
||||
margin-right: 10px;
|
||||
|
||||
.checkbox {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.checked {
|
||||
border-color: #00b96b;
|
||||
background: rgba(0, 185, 107, 0.2);
|
||||
color: #00b96b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-img {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 12px;
|
||||
margin-right: 20px;
|
||||
background: #000;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
height: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.item-name {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
font-size: 24px;
|
||||
color: #888;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
|
||||
.price {
|
||||
font-size: 32px;
|
||||
color: #00b96b;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.quantity-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
|
||||
.btn-qty {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
color: #fff;
|
||||
|
||||
&:active { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.qty-num {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
padding: 10px;
|
||||
margin-left: 10px;
|
||||
color: #ff4d4f;
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 110px;
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 30px;
|
||||
z-index: 100;
|
||||
|
||||
.left-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.select-all-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 30px;
|
||||
|
||||
.checkbox {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #666;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.checked {
|
||||
border-color: #00b96b;
|
||||
background: rgba(0, 185, 107, 0.2);
|
||||
color: #00b96b;
|
||||
}
|
||||
}
|
||||
|
||||
.label { font-size: 28px; color: #fff; }
|
||||
}
|
||||
|
||||
.total-info {
|
||||
.label { font-size: 24px; color: #888; margin-right: 10px; }
|
||||
.price { font-size: 40px; color: #00b96b; font-weight: bold; }
|
||||
}
|
||||
}
|
||||
|
||||
.btn-checkout {
|
||||
background: linear-gradient(135deg, #00b96b 0%, #00f0ff 100%);
|
||||
color: #000;
|
||||
border-radius: 40px;
|
||||
padding: 0 50px;
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
box-shadow: 0 0 20px rgba(0, 185, 107, 0.3);
|
||||
|
||||
&:active { transform: scale(0.98); }
|
||||
&.disabled {
|
||||
background: #333;
|
||||
color: #666;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.empty { color: #999; font-size: 16px; }
|
||||
|
||||
@@ -1,12 +1,145 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { View, Text, Image, ScrollView, Button } from '@tarojs/components'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { getCart, updateQuantity, removeItem, toggleSelect, toggleSelectAll, CartItem } from '../../utils/cart'
|
||||
import './cart.scss'
|
||||
|
||||
export default function Cart() {
|
||||
const [cartItems, setCartItems] = useState<CartItem[]>([])
|
||||
|
||||
useDidShow(() => {
|
||||
refreshCart()
|
||||
})
|
||||
|
||||
const refreshCart = () => {
|
||||
setCartItems(getCart())
|
||||
}
|
||||
|
||||
const handleUpdateQuantity = (id: number, delta: number) => {
|
||||
const item = cartItems.find(i => i.id === id)
|
||||
if (!item) return
|
||||
const newQty = item.quantity + delta
|
||||
if (newQty < 1) return
|
||||
const newCart = updateQuantity(id, newQty)
|
||||
setCartItems(newCart)
|
||||
}
|
||||
|
||||
const handleRemove = (id: number) => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该商品吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const newCart = removeItem(id)
|
||||
setCartItems(newCart)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleToggle = (id: number) => {
|
||||
const newCart = toggleSelect(id)
|
||||
setCartItems(newCart)
|
||||
}
|
||||
|
||||
const isAllSelected = useMemo(() => {
|
||||
return cartItems.length > 0 && cartItems.every(i => i.selected)
|
||||
}, [cartItems])
|
||||
|
||||
const handleToggleAll = () => {
|
||||
const newCart = toggleSelectAll(!isAllSelected)
|
||||
setCartItems(newCart)
|
||||
}
|
||||
|
||||
const selectedCount = useMemo(() => {
|
||||
return cartItems.filter(i => i.selected).reduce((sum, i) => sum + i.quantity, 0)
|
||||
}, [cartItems])
|
||||
|
||||
const totalPrice = useMemo(() => {
|
||||
return cartItems.filter(i => i.selected).reduce((sum, i) => sum + i.price * i.quantity, 0)
|
||||
}, [cartItems])
|
||||
|
||||
const handleCheckout = () => {
|
||||
if (selectedCount === 0) {
|
||||
Taro.showToast({ title: '请选择商品', icon: 'none' })
|
||||
return
|
||||
}
|
||||
Taro.navigateTo({
|
||||
url: '/pages/order/checkout?from=cart'
|
||||
})
|
||||
}
|
||||
|
||||
const goShopping = () => {
|
||||
Taro.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='page-container'>
|
||||
<View className='empty'>
|
||||
<Text>购物车功能即将上线</Text>
|
||||
</View>
|
||||
{cartItems.length === 0 ? (
|
||||
<View className='empty-state'>
|
||||
<Text className='empty-icon'>🛒</Text>
|
||||
<Text className='empty-text'>购物车空空如也</Text>
|
||||
<Button onClick={goShopping} style={{marginTop: 20, background: '#00b96b', color: '#fff'}}>去逛逛</Button>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView scrollY className='cart-list'>
|
||||
{cartItems.map(item => (
|
||||
<View key={item.id} className='cart-item'>
|
||||
<View className='checkbox-area' onClick={() => handleToggle(item.id)}>
|
||||
<View className={`checkbox ${item.selected ? 'checked' : ''}`}>
|
||||
{item.selected && <Text>✓</Text>}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Image src={item.image} className='item-img' mode='aspectFill' />
|
||||
|
||||
<View className='item-info'>
|
||||
<View>
|
||||
<Text className='item-name'>{item.name}</Text>
|
||||
{/* <Text className='item-desc'>{item.description}</Text> */}
|
||||
</View>
|
||||
|
||||
<View className='price-row'>
|
||||
<Text className='price'>¥{item.price}</Text>
|
||||
|
||||
<View className='quantity-control'>
|
||||
<View className='btn-qty' onClick={() => handleUpdateQuantity(item.id, -1)}>−</View>
|
||||
<Text className='qty-num'>{item.quantity}</Text>
|
||||
<View className='btn-qty' onClick={() => handleUpdateQuantity(item.id, 1)}>+</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='btn-delete' onClick={() => handleRemove(item.id)}>×</View>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{cartItems.length > 0 && (
|
||||
<View className='bottom-bar'>
|
||||
<View className='left-section'>
|
||||
<View className='select-all-btn' onClick={handleToggleAll}>
|
||||
<View className={`checkbox ${isAllSelected ? 'checked' : ''}`}>
|
||||
{isAllSelected && <Text>✓</Text>}
|
||||
</View>
|
||||
<Text className='label'>全选</Text>
|
||||
</View>
|
||||
|
||||
<View className='total-info'>
|
||||
<Text className='label'>合计:</Text>
|
||||
<Text className='price'>¥{totalPrice}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
className={`btn-checkout ${selectedCount === 0 ? 'disabled' : ''}`}
|
||||
onClick={handleCheckout}
|
||||
>
|
||||
去结算({selectedCount})
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,28 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
align-items: center;
|
||||
|
||||
.tag {
|
||||
background: rgba(0, 240, 255, 0.2);
|
||||
color: #00f0ff;
|
||||
padding: 6px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 24px;
|
||||
border: 1px solid #00f0ff;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #888;
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #aaa;
|
||||
font-size: 28px;
|
||||
@@ -20,7 +42,7 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ar-placeholder {
|
||||
.course-placeholder {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background: #111;
|
||||
70
miniprogram/src/pages/courses/detail.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { View, Text, Button, Image } from '@tarojs/components'
|
||||
import Taro, { useLoad } from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import { getVBCourseDetail } from '../../api'
|
||||
import './detail.scss'
|
||||
|
||||
export default function CourseDetail() {
|
||||
const [detail, setDetail] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useLoad((options) => {
|
||||
if (options.id) fetchDetail(options.id)
|
||||
})
|
||||
|
||||
const typeMap: Record<string, string> = {
|
||||
software: '软件课程',
|
||||
hardware: '硬件课程',
|
||||
incubation: '产品商业孵化'
|
||||
}
|
||||
|
||||
const fetchDetail = async (id: string) => {
|
||||
try {
|
||||
const res: any = await getVBCourseDetail(Number(id))
|
||||
setDetail(res)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLaunch = () => {
|
||||
Taro.showToast({
|
||||
title: '课程内容准备中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
if (loading) return <View className='page-container'><Text style={{color:'#fff'}}>Loading...</Text></View>
|
||||
if (!detail) return <View className='page-container'><Text style={{color:'#fff'}}>Not Found</Text></View>
|
||||
|
||||
return (
|
||||
<View className='page-container'>
|
||||
<Text className='title'>{detail.title}</Text>
|
||||
|
||||
<View className='meta-info'>
|
||||
<Text className='tag'>{typeMap[detail.course_type] || '软件课程'}</Text>
|
||||
<Text className='info'>讲师: {detail.instructor}</Text>
|
||||
<Text className='info'>时长: {detail.duration}</Text>
|
||||
<Text className='info'>课时: {detail.lesson_count}</Text>
|
||||
</View>
|
||||
|
||||
<Text className='desc'>{detail.description}</Text>
|
||||
|
||||
<View className='course-placeholder'>
|
||||
{detail.display_detail_image ? (
|
||||
<Image src={detail.display_detail_image} style={{ width: '100%', height: '100%', borderRadius: '16px' }} mode='widthFix' />
|
||||
) : (
|
||||
<>
|
||||
<Text className='icon'>📚</Text>
|
||||
<Text className='text'>课程大纲与视频内容加载区域</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Button className='btn-launch' onClick={handleLaunch}>开始学习</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -68,6 +68,33 @@
|
||||
font-size: 80px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tag-container {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
background: rgba(0, 240, 255, 0.2);
|
||||
border: 1px solid #00f0ff;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
.type-text {
|
||||
color: #00f0ff;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&.special {
|
||||
background: rgba(255, 87, 34, 0.2);
|
||||
border: 1px solid #ff5722;
|
||||
.type-text {
|
||||
color: #ff5722;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -79,6 +106,16 @@
|
||||
margin-bottom: 15px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 10px;
|
||||
.info-text {
|
||||
color: #aaa;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
color: #888;
|
||||
@@ -1,21 +1,21 @@
|
||||
import { View, Text, Image, Button } from '@tarojs/components'
|
||||
import Taro, { useLoad } from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import { getARServices } from '../../api'
|
||||
import { getVBCourses } from '../../api'
|
||||
import './index.scss'
|
||||
|
||||
export default function ARIndex() {
|
||||
const [arList, setArList] = useState<any[]>([])
|
||||
export default function CourseIndex() {
|
||||
const [courseList, setCourseList] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useLoad(() => {
|
||||
fetchAR()
|
||||
fetchCourses()
|
||||
})
|
||||
|
||||
const fetchAR = async () => {
|
||||
const fetchCourses = async () => {
|
||||
try {
|
||||
const res: any = await getARServices()
|
||||
setArList(res.results || res)
|
||||
const res: any = await getVBCourses()
|
||||
setCourseList(res.results || res)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
@@ -25,7 +25,7 @@ export default function ARIndex() {
|
||||
}
|
||||
|
||||
const goDetail = (id: number) => {
|
||||
Taro.navigateTo({ url: `/pages/ar/detail?id=${id}` })
|
||||
Taro.navigateTo({ url: `/pages/courses/detail?id=${id}` })
|
||||
}
|
||||
|
||||
if (loading) return <View className='page-container'><Text style={{color:'#fff'}}>Loading...</Text></View>
|
||||
@@ -35,29 +35,29 @@ export default function ARIndex() {
|
||||
<View className='bg-decoration' />
|
||||
|
||||
<View className='header'>
|
||||
<Text className='title'>AR <Text className='highlight'>UNIVERSE</Text></Text>
|
||||
<Text className='desc'>探索全息增强现实体验</Text>
|
||||
<Text className='title'>VB <Text className='highlight'>COURSES</Text></Text>
|
||||
<Text className='desc'>探索 VB 编程课程</Text>
|
||||
</View>
|
||||
|
||||
<View className='ar-grid'>
|
||||
{arList.length === 0 ? (
|
||||
{courseList.length === 0 ? (
|
||||
<View style={{ width: '100%', textAlign: 'center', color: '#666', marginTop: 50 }}>
|
||||
<Text>暂无 AR 体验内容</Text>
|
||||
<Text>暂无 VB 课程内容</Text>
|
||||
</View>
|
||||
) : (
|
||||
arList.map((item) => (
|
||||
courseList.map((item) => (
|
||||
<View key={item.id} className='ar-card' onClick={() => goDetail(item.id)}>
|
||||
<View className='cover-box'>
|
||||
{item.cover_image_url ? (
|
||||
<Image src={item.cover_image_url} className='cover-img' mode='aspectFill' />
|
||||
) : (
|
||||
<Text className='placeholder-icon'>AR</Text>
|
||||
<Text className='placeholder-icon'>VB</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className='content'>
|
||||
<Text className='item-title'>{item.title}</Text>
|
||||
<Text className='item-desc'>{item.description}</Text>
|
||||
<Button className='btn-start'>启动体验</Button>
|
||||
<Button className='btn-start'>开始学习</Button>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
@@ -1,8 +1,10 @@
|
||||
.page-container {
|
||||
height: 100vh;
|
||||
background-color: #000;
|
||||
min-height: 100vh;
|
||||
background-color: #050505;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.loading-screen, .error-screen {
|
||||
@@ -10,147 +12,259 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
color: #00f0ff;
|
||||
background: #000;
|
||||
font-size: 28px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100vh;
|
||||
background: #000;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-bottom: 200px; // Ensure scroll space for bottom bar
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
// Animations
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(40px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-20px); }
|
||||
100% { transform: translateY(0px); }
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0% { box-shadow: 0 0 10px rgba(0, 185, 107, 0.4); }
|
||||
50% { box-shadow: 0 0 25px rgba(0, 185, 107, 0.8), 0 0 10px rgba(0, 240, 255, 0.4); }
|
||||
100% { box-shadow: 0 0 10px rgba(0, 185, 107, 0.4); }
|
||||
}
|
||||
|
||||
@keyframes scanline {
|
||||
0% { top: -10%; opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
100% { top: 110%; opacity: 0; }
|
||||
}
|
||||
|
||||
// Hero Section
|
||||
.hero-section {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 40px;
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
|
||||
.image-container {
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
background: radial-gradient(circle at center, #1a1a1a, #000);
|
||||
min-height: 600px; // Slightly reduced to fit better
|
||||
background: radial-gradient(circle at center, rgba(0, 240, 255, 0.05) 0%, transparent 70%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
// Scanline effect
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(to right, transparent, rgba(0, 240, 255, 0.5), transparent);
|
||||
animation: scanline 3s linear infinite;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.hero-img {
|
||||
width: 100%;
|
||||
width: 75%;
|
||||
height: auto;
|
||||
display: block;
|
||||
filter: drop-shadow(0 0 40px rgba(0, 240, 255, 0.2));
|
||||
animation: float 6s ease-in-out infinite;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.placeholder-box {
|
||||
.icon-bolt { font-size: 100px; }
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 60%;
|
||||
background: linear-gradient(to top, #000 10%, transparent);
|
||||
.icon-bolt { font-size: 150px; color: #00b96b; text-shadow: 0 0 30px rgba(0, 185, 107, 0.5); }
|
||||
}
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
padding: 0 30px;
|
||||
margin-top: -100px; // Pull up over image
|
||||
padding: 0 40px;
|
||||
margin-top: -40px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.hero-title {
|
||||
font-size: 48px;
|
||||
font-size: 60px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: 0 0 20px rgba(0,0,0,0.8);
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.1;
|
||||
text-shadow: 0 0 20px rgba(0, 240, 255, 0.3);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
font-size: 28px;
|
||||
color: #ccc;
|
||||
line-height: 1.5;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
line-height: 1.6;
|
||||
display: block;
|
||||
margin-bottom: 25px;
|
||||
text-shadow: 0 0 10px rgba(0,0,0,0.8);
|
||||
margin-bottom: 32px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.tags-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
gap: 16px;
|
||||
|
||||
.tag {
|
||||
padding: 8px 20px;
|
||||
border-radius: 30px;
|
||||
padding: 10px 28px;
|
||||
border-radius: 4px; // Techy sharp corners
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&.cyan { background: rgba(0, 240, 255, 0.15); color: #00f0ff; border: 1px solid rgba(0, 240, 255, 0.3); }
|
||||
&.blue { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); }
|
||||
&.purple { background: rgba(168, 85, 247, 0.15); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.3); }
|
||||
// Tech border effect
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 4px; height: 100%;
|
||||
}
|
||||
|
||||
&.cyan {
|
||||
color: #00f0ff;
|
||||
background: rgba(0, 240, 255, 0.08);
|
||||
border: 1px solid rgba(0, 240, 255, 0.3);
|
||||
&::before { background: #00f0ff; }
|
||||
}
|
||||
&.blue {
|
||||
color: #3b82f6;
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
&::before { background: #3b82f6; }
|
||||
}
|
||||
&.purple {
|
||||
color: #a855f7;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
||||
&::before { background: #a855f7; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stats Card (HUD Style)
|
||||
.stats-card {
|
||||
margin: 0 30px 40px;
|
||||
border-radius: 24px;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
.stat-label { font-size: 24px; color: #888; display: block; margin-bottom: 10px; }
|
||||
.stat-value { font-size: 36px; font-weight: bold; color: #fff; }
|
||||
.price { color: #00b96b; text-shadow: 0 0 10px rgba(0, 185, 107, 0.3); }
|
||||
.low-stock { color: #ff4d4f; }
|
||||
margin: 40px 40px 60px;
|
||||
padding: 30px !important;
|
||||
background: rgba(20, 20, 20, 0.6) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
animation: fadeInUp 0.8s ease-out 0.2s backwards;
|
||||
|
||||
// Corner accents
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -1px; left: -1px;
|
||||
width: 20px; height: 20px;
|
||||
border-top: 2px solid #00b96b;
|
||||
border-left: 2px solid #00b96b;
|
||||
border-top-left-radius: 12px;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px; right: -1px;
|
||||
width: 20px; height: 20px;
|
||||
border-bottom: 2px solid #00b96b;
|
||||
border-right: 2px solid #00b96b;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
|
||||
.label-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
.label { font-size: 24px; color: #666; flex: 1; text-transform: uppercase; letter-spacing: 1px; }
|
||||
}
|
||||
|
||||
.value-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: baseline;
|
||||
|
||||
.price-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
.symbol { font-size: 32px; color: #00b96b; font-weight: bold; margin-right: 4px; }
|
||||
.price {
|
||||
font-size: 72px;
|
||||
color: #00b96b;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 25px rgba(0, 185, 107, 0.4);
|
||||
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; // Ensure clean number font
|
||||
}
|
||||
}
|
||||
|
||||
.stock-box {
|
||||
.stock { font-size: 36px; color: #fff; font-weight: bold; }
|
||||
.unit { font-size: 24px; color: #666; margin-left: 6px; }
|
||||
}
|
||||
}
|
||||
|
||||
.divider { width: 1px; height: 60px; background: rgba(255,255,255,0.1); }
|
||||
}
|
||||
|
||||
// Features Section
|
||||
.features-section {
|
||||
padding: 0 30px;
|
||||
padding: 0 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
gap: 40px;
|
||||
margin-bottom: 60px;
|
||||
|
||||
.feature-card {
|
||||
padding: 30px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: row; // Change to row for better list layout
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
background: rgba(255, 255, 255, 0.03) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05) !important;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
// Stagger animations manually or via JS (here simplified)
|
||||
|
||||
.feature-icon-box {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-right: 25px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-right: 30px;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 16px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.f-icon { font-size: 40px; color: #00f0ff; }
|
||||
.f-icon-img { width: 50px; height: 50px; }
|
||||
.f-icon { font-size: 50px; color: #00b96b; }
|
||||
.f-icon-img { width: 60px; height: 60px; object-fit: contain; }
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
flex: 1;
|
||||
.f-title { font-size: 30px; font-weight: bold; color: #fff; margin-bottom: 10px; display: block; }
|
||||
.f-desc { font-size: 24px; color: #aaa; line-height: 1.5; }
|
||||
.f-title { font-size: 32px; font-weight: bold; color: #fff; margin-bottom: 10px; display: block; }
|
||||
.f-desc { font-size: 24px; color: #888; line-height: 1.5; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,66 +272,94 @@
|
||||
.detail-image-section {
|
||||
width: 100%;
|
||||
margin-bottom: 40px;
|
||||
.long-detail-img { width: 100%; display: block; }
|
||||
position: relative;
|
||||
|
||||
// Decorative line top
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 4px;
|
||||
background: #333;
|
||||
margin: 0 auto 40px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.long-detail-img { width: 100%; height: auto; display: block; }
|
||||
}
|
||||
|
||||
.footer-spacer { height: 160px; }
|
||||
.footer-spacer { height: 200px; }
|
||||
|
||||
// Bottom Bar
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 20px 30px;
|
||||
bottom: 40px;
|
||||
left: 30px;
|
||||
right: 30px;
|
||||
height: 110px;
|
||||
z-index: 100;
|
||||
border-top-left-radius: 30px;
|
||||
border-top-right-radius: 30px;
|
||||
background: rgba(20, 20, 20, 0.95); // Darker for contrast
|
||||
border-radius: 55px; // Fully rounded capsule
|
||||
background: rgba(20, 20, 20, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 10px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.action-row {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
height: 100px;
|
||||
|
||||
.cart-icon-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 20px;
|
||||
|
||||
.icon { font-size: 40px; margin-bottom: 5px; }
|
||||
.label { font-size: 20px; color: #888; }
|
||||
}
|
||||
|
||||
.btn-add-cart, .btn-buy-now {
|
||||
.btn-add-cart {
|
||||
flex: 1;
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
border-radius: 40px;
|
||||
font-size: 28px;
|
||||
height: 100%;
|
||||
border-radius: 45px 0 0 45px;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
margin: 0;
|
||||
|
||||
&::after { border: none; }
|
||||
}
|
||||
|
||||
.btn-add-cart {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active { background: rgba(255, 255, 255, 0.2); }
|
||||
}
|
||||
|
||||
|
||||
.btn-buy-now {
|
||||
background: linear-gradient(90deg, #00b96b, #00f0ff);
|
||||
color: #000;
|
||||
box-shadow: 0 5px 20px rgba(0, 185, 107, 0.3);
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
border-radius: 0 45px 45px 0;
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
border: none;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #00b96b 0%, #00f0ff 100%);
|
||||
color: #000; // Black text for high contrast on neon
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: pulse-glow 3s infinite;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.cart-icon {
|
||||
font-size: 36px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
padding-bottom: calc(20px + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(20px + env(safe-area-inset-bottom));
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { View, Text, Image, ScrollView, Button } from '@tarojs/components'
|
||||
import Taro, { useRouter, useLoad } from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import { getConfigDetail } from '../../api'
|
||||
import ParticleBackground from '../../components/ParticleBackground'
|
||||
import { addToCart } from '../../utils/cart'
|
||||
import './detail.scss'
|
||||
|
||||
export default function Detail() {
|
||||
@@ -26,6 +28,11 @@ export default function Detail() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddToCart = () => {
|
||||
if (!product) return
|
||||
addToCart(product)
|
||||
}
|
||||
|
||||
const buyNow = () => {
|
||||
if (!product) return
|
||||
Taro.navigateTo({
|
||||
@@ -38,12 +45,13 @@ export default function Detail() {
|
||||
|
||||
return (
|
||||
<View className='page-container'>
|
||||
<ParticleBackground />
|
||||
<ScrollView scrollY className='content'>
|
||||
{/* Hero Section */}
|
||||
<View className='hero-section'>
|
||||
<View className='image-container'>
|
||||
{product.detail_image_url || product.static_image_url ? (
|
||||
<Image src={product.detail_image_url || product.static_image_url} mode='widthFix' className='hero-img' />
|
||||
{product.static_image_url ? (
|
||||
<Image src={product.static_image_url} mode='widthFix' className='hero-img' />
|
||||
) : (
|
||||
<View className='placeholder-box'>
|
||||
<Text className='icon-bolt'>⚡</Text>
|
||||
@@ -65,44 +73,84 @@ export default function Detail() {
|
||||
</View>
|
||||
|
||||
{/* Stats Section */}
|
||||
<View className='stats-card glass-panel'>
|
||||
<View className='stat-item'>
|
||||
<Text className='stat-label'>售价</Text>
|
||||
<Text className='stat-value price'>¥{product.price}</Text>
|
||||
<View className='stats-card'>
|
||||
<View className='label-row'>
|
||||
<Text className='label'>售价</Text>
|
||||
<Text className='label' style={{textAlign: 'right'}}>库存</Text>
|
||||
</View>
|
||||
<View className='divider' />
|
||||
<View className='stat-item'>
|
||||
<Text className='stat-label'>库存</Text>
|
||||
<Text className={`stat-value ${product.stock < 10 ? 'low-stock' : ''}`}>{product.stock}件</Text>
|
||||
<View className='value-row'>
|
||||
<View className='price-box'>
|
||||
<Text className='symbol'>¥</Text>
|
||||
<Text className='price'>{product.price}</Text>
|
||||
</View>
|
||||
<View className='stock-box'>
|
||||
<Text className='stock'>{product.stock}</Text>
|
||||
<Text className='unit'>件</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Features Section */}
|
||||
<View className='features-section'>
|
||||
{product.features && product.features.length > 0 ? (
|
||||
product.features.map((f, idx) => (
|
||||
<View key={idx} className='feature-card glass-panel'>
|
||||
<View className='feature-icon-box'>
|
||||
{f.icon_url ? <Image src={f.icon_url} className='f-icon-img' /> : <Text className='f-icon'>★</Text>}
|
||||
product.features.map((f, idx) => {
|
||||
let iconContent
|
||||
if (f.display_icon) {
|
||||
iconContent = <Image src={f.display_icon} className='f-icon-img' />
|
||||
} else if (f.icon_url) {
|
||||
iconContent = <Image src={f.icon_url} className='f-icon-img' />
|
||||
} else {
|
||||
let iconChar = '⭐'
|
||||
let iconColor = '#00b96b'
|
||||
switch(f.icon_name) {
|
||||
case 'SafetyCertificate': iconChar = '🛡'; break;
|
||||
case 'Eye': iconChar = '👁'; iconColor = '#3b82f6'; break;
|
||||
case 'Thunderbolt': iconChar = '⚡'; iconColor = '#faad14'; break;
|
||||
default: break;
|
||||
}
|
||||
iconContent = <Text className='f-icon' style={{color: iconColor}}>{iconChar}</Text>
|
||||
}
|
||||
|
||||
return (
|
||||
<View key={idx} className='feature-card'>
|
||||
<View className='feature-icon-box'>
|
||||
{iconContent}
|
||||
</View>
|
||||
<View className='feature-text'>
|
||||
<Text className='f-title'>{f.title}</Text>
|
||||
<Text className='f-desc'>{f.description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='feature-text'>
|
||||
<Text className='f-title'>{f.title}</Text>
|
||||
<Text className='f-desc'>{f.description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<View className='feature-card glass-panel'>
|
||||
<Text className='f-title'>极致性能释放</Text>
|
||||
<Text className='f-desc'>{product.chip_type} 强劲核心,提供强大的边缘计算算力支持。</Text>
|
||||
</View>
|
||||
<>
|
||||
<View className='feature-card'>
|
||||
<View className='feature-icon-box'>
|
||||
<Text className='f-icon'>🛡</Text>
|
||||
</View>
|
||||
<View className='feature-text'>
|
||||
<Text className='f-title'>工业级安全标准</Text>
|
||||
<Text className='f-desc'>采用军工级加密芯片,保障您的数据隐私安全。无论是边缘计算还是云端同步,全程加密传输。</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='feature-card'>
|
||||
<View className='feature-icon-box'>
|
||||
<Text className='f-icon' style={{color: '#3b82f6'}}>👁</Text>
|
||||
</View>
|
||||
<View className='feature-text'>
|
||||
<Text className='f-title' style={{color: '#3b82f6'}}>超清视觉感知</Text>
|
||||
<Text className='f-desc'>搭载 4K 高清摄像头与 AI 视觉算法,实时捕捉每一个细节。支持人脸识别、物体检测等。</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Detail Image */}
|
||||
{product.detail_image_url && (
|
||||
{(product.display_detail_image || product.detail_image_url) && (
|
||||
<View className='detail-image-section'>
|
||||
<Image src={product.detail_image_url} mode='widthFix' className='long-detail-img' />
|
||||
<Image src={product.display_detail_image || product.detail_image_url} mode='widthFix' className='long-detail-img' />
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -110,14 +158,15 @@ export default function Detail() {
|
||||
</ScrollView>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<View className='bottom-bar glass-panel safe-area-bottom'>
|
||||
<View className='bottom-bar'>
|
||||
<View className='action-row'>
|
||||
<View className='cart-icon-btn' onClick={() => Taro.switchTab({ url: '/pages/cart/cart' })}>
|
||||
<Text className='icon'>🛒</Text>
|
||||
<Text className='label'>购物车</Text>
|
||||
</View>
|
||||
<Button className='btn-add-cart' onClick={() => Taro.showToast({title: '加入购物车', icon:'none'})}>加入购物车</Button>
|
||||
<Button className='btn-buy-now' onClick={buyNow}>立即购买</Button>
|
||||
<Button className='btn-add-cart' onClick={handleAddToCart}>
|
||||
<Text>加入购物车</Text>
|
||||
</Button>
|
||||
<Button className='btn-buy-now' onClick={buyNow}>
|
||||
<Text className='cart-icon'>🛒</Text>
|
||||
<Text>立即购买</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,52 +1,79 @@
|
||||
.page-container {
|
||||
height: 100vh;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
background-color: var(--bg-dark);
|
||||
color: var(--text-main);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
// Ambient Light 1 (Cyan)
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
width: 60%;
|
||||
height: 40%;
|
||||
background: radial-gradient(circle, rgba(0, 240, 255, 0.15) 0%, transparent 70%);
|
||||
filter: blur(80px);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Ambient Light 2 (Green/Purple mix)
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 10%;
|
||||
right: -10%;
|
||||
width: 50%;
|
||||
height: 40%;
|
||||
background: radial-gradient(circle, rgba(189, 0, 255, 0.1) 0%, transparent 70%);
|
||||
filter: blur(80px);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
// Ensure no padding here
|
||||
}
|
||||
|
||||
.scroll-inner {
|
||||
// Container for scroll content
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
padding: 60px 20px 40px;
|
||||
padding: 80px 24px 60px; // 增加头部留白
|
||||
position: relative;
|
||||
|
||||
.logo-box {
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.logo-img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin-bottom: 15px;
|
||||
filter: drop-shadow(0 0 15px rgba(0, 240, 255, 0.4));
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
margin-bottom: 20px;
|
||||
filter: drop-shadow(0 0 25px rgba(0, 240, 255, 0.5));
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 40px;
|
||||
font-size: 48px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
letter-spacing: 6px;
|
||||
text-shadow: 0 0 20px rgba(0, 240, 255, 0.6);
|
||||
letter-spacing: 8px;
|
||||
text-shadow: 0 0 30px rgba(0, 240, 255, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.title-container {
|
||||
margin-bottom: 25px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -54,26 +81,27 @@
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #00f0ff;
|
||||
text-shadow: 0 0 15px rgba(0, 240, 255, 0.5);
|
||||
font-size: 40px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-cyan);
|
||||
text-shadow: 0 0 20px rgba(0, 240, 255, 0.4);
|
||||
}
|
||||
|
||||
.cursor {
|
||||
font-size: 36px;
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
margin-left: 8px;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #aaa;
|
||||
font-size: 26px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
font-size: 28px;
|
||||
line-height: 1.8; // 增加行高
|
||||
display: block;
|
||||
padding: 0 40px;
|
||||
font-weight: 300;
|
||||
font-weight: 400;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,40 +110,54 @@
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
.status-box {
|
||||
padding: 100px 0;
|
||||
text-align: center;
|
||||
|
||||
.loading-text { color: #00f0ff; font-size: 28px; }
|
||||
.error-text { color: #ff4d4f; font-size: 28px; margin-bottom: 20px; display: block;}
|
||||
.btn-retry { background: rgba(255,255,255,0.1); color: #fff; font-size: 24px; padding: 10px 40px; display: inline-block;}
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.product-grid {
|
||||
padding: 0 30px;
|
||||
padding: 0 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
gap: 48px; // 增加卡片间距
|
||||
}
|
||||
|
||||
// 玻璃态卡片升级版
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border-radius: 32px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
transition: all 0.3s ease;
|
||||
box-shadow:
|
||||
0 20px 40px rgba(0, 0, 0, 0.4),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.05); // 内描边增强质感
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
position: relative;
|
||||
|
||||
// 高光反射效果
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
border-color: #00b96b;
|
||||
box-shadow: 0 0 30px rgba(0, 185, 107, 0.2);
|
||||
transform: scale(0.96);
|
||||
box-shadow:
|
||||
0 10px 20px rgba(0, 0, 0, 0.4),
|
||||
0 0 30px rgba(0, 240, 255, 0.1); // 按压发光
|
||||
border-color: rgba(0, 240, 255, 0.3);
|
||||
}
|
||||
|
||||
&-cover {
|
||||
height: 360px;
|
||||
height: 400px; // 加大图片区域
|
||||
background: #111;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -123,7 +165,8 @@
|
||||
.card-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.5s ease;
|
||||
object-fit: cover;
|
||||
transition: transform 0.6s ease;
|
||||
}
|
||||
|
||||
.placeholder-img {
|
||||
@@ -132,53 +175,85 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(circle at center, #222, #111);
|
||||
.icon-rocket { font-size: 100px; }
|
||||
}
|
||||
background: radial-gradient(circle at center, #1a1a1a, #050505);
|
||||
|
||||
.radar-scan {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 2px solid rgba(0, 240, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: var(--primary-cyan);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px var(--primary-cyan);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(from 0deg, transparent 0%, transparent 60%, rgba(0, 240, 255, 0.4) 100%);
|
||||
animation: radar-spin 2s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
|
||||
height: 60%;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.9), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
padding: 30px;
|
||||
padding: 40px 32px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.card-title {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
font-size: 40px; // 加大标题
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
line-height: 1.3;
|
||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
line-height: 1.2;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 36px;
|
||||
color: #00b96b;
|
||||
font-weight: 900;
|
||||
text-shadow: 0 0 15px rgba(0, 185, 107, 0.3);
|
||||
color: var(--primary-cyan); // 统一用青色或根据产品类型变化
|
||||
font-weight: 800;
|
||||
text-shadow: 0 0 20px rgba(0, 240, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&-desc {
|
||||
font-size: 26px;
|
||||
color: #ccc;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 25px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 32px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
@@ -188,52 +263,126 @@
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 30px;
|
||||
gap: 16px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.tag {
|
||||
padding: 8px 18px;
|
||||
border-radius: 12px;
|
||||
padding: 10px 24px;
|
||||
border-radius: 16px;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&.cyan {
|
||||
color: #00f0ff;
|
||||
background: rgba(0, 240, 255, 0.1);
|
||||
border: 1px solid rgba(0, 240, 255, 0.3);
|
||||
color: var(--primary-cyan);
|
||||
background: rgba(0, 240, 255, 0.08);
|
||||
border: 1px solid rgba(0, 240, 255, 0.2);
|
||||
}
|
||||
&.blue {
|
||||
color: #3b82f6;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
&.purple {
|
||||
color: #a855f7;
|
||||
background: rgba(168, 85, 247, 0.1);
|
||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
||||
color: var(--primary-purple);
|
||||
background: rgba(189, 0, 255, 0.08);
|
||||
border: 1px solid rgba(189, 0, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
.btn-buy {
|
||||
background: linear-gradient(90deg, #00b96b, #00f0ff);
|
||||
background: linear-gradient(90deg, var(--primary-green), var(--primary-cyan));
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
font-weight: 800;
|
||||
font-size: 30px;
|
||||
border-radius: 50px;
|
||||
border-radius: 60px; // 更圆润
|
||||
border: none;
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
box-shadow: 0 5px 20px rgba(0, 185, 107, 0.3);
|
||||
height: 90px;
|
||||
line-height: 90px;
|
||||
box-shadow: 0 10px 30px rgba(0, 185, 107, 0.25);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// 流光效果
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
|
||||
animation: shimmer 3s infinite;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 5px 15px rgba(0, 185, 107, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-spacer {
|
||||
height: 100px;
|
||||
@keyframes shimmer {
|
||||
0% { left: -100%; }
|
||||
20% { left: 100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
@keyframes radar-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.footer-spacer {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
// 骨架屏样式
|
||||
.skeleton-wrapper {
|
||||
padding: 0 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 48px;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
height: 700px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 32px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.03), transparent);
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
// 列表入场动画
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +65,10 @@ export default function Index() {
|
||||
</View>
|
||||
|
||||
{loading ? (
|
||||
<View className='status-box'>
|
||||
<Text className='loading-text'>正在加载硬件配置...</Text>
|
||||
<View className='skeleton-wrapper'>
|
||||
{[1, 2, 3].map(i => (
|
||||
<View key={i} className='skeleton-card' />
|
||||
))}
|
||||
</View>
|
||||
) : error ? (
|
||||
<View className='status-box'>
|
||||
@@ -75,14 +77,19 @@ export default function Index() {
|
||||
</View>
|
||||
) : (
|
||||
<View className='product-grid'>
|
||||
{products.map((item) => (
|
||||
<View key={item.id} className='card' onClick={() => goToDetail(item.id)}>
|
||||
{products.map((item, index) => (
|
||||
<View
|
||||
key={item.id}
|
||||
className='card fade-in-up'
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
onClick={() => goToDetail(item.id)}
|
||||
>
|
||||
<View className='card-cover'>
|
||||
{item.static_image_url ? (
|
||||
<Image src={item.static_image_url} mode='aspectFill' className='card-img' />
|
||||
) : (
|
||||
<View className='placeholder-img'>
|
||||
<Text className='icon-rocket'>🚀</Text>
|
||||
<View className='radar-scan'></View>
|
||||
</View>
|
||||
)}
|
||||
<View className='card-overlay' />
|
||||
|
||||
@@ -1,51 +1,154 @@
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f7f8fa;
|
||||
padding: 15px;
|
||||
padding-bottom: 80px;
|
||||
background-color: #050505;
|
||||
color: #fff;
|
||||
padding-bottom: 120px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.02);
|
||||
margin: 20px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
|
||||
.section-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 24px;
|
||||
background: #00b96b;
|
||||
margin-right: 12px;
|
||||
vertical-align: middle;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delivery-type-section {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
|
||||
.type-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
font-size: 28px;
|
||||
color: #888;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #00b96b;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.address-section {
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.row {
|
||||
margin-bottom: 8px;
|
||||
.name { font-size: 16px; font-weight: bold; margin-right: 10px; }
|
||||
.phone { font-size: 14px; color: #666; }
|
||||
}
|
||||
.addr { font-size: 14px; color: #333; line-height: 1.4; }
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.placeholder-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
.address-info {
|
||||
flex: 1;
|
||||
.user-info {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
.phone { margin-left: 20px; color: #888; font-weight: normal; font-size: 26px; }
|
||||
}
|
||||
.address-text {
|
||||
font-size: 26px;
|
||||
color: #aaa;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.placeholder {
|
||||
color: #00b96b;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 30px;
|
||||
color: #666;
|
||||
margin-left: 20px;
|
||||
}
|
||||
.placeholder { font-size: 16px; color: #00b96b; }
|
||||
}
|
||||
|
||||
.product-section {
|
||||
.p-name { font-size: 16px; font-weight: 500; margin-bottom: 10px; display: block; }
|
||||
.row { display: flex; justify-content: space-between; align-items: center; }
|
||||
.p-price { font-size: 16px; color: #333; }
|
||||
.p-qty { font-size: 14px; color: #999; }
|
||||
|
||||
.divider { height: 1px; background: #eee; margin: 15px 0; }
|
||||
|
||||
.total-row {
|
||||
.total-price { font-size: 20px; color: #ff4d4f; font-weight: bold; }
|
||||
}
|
||||
padding: 0; // Remove padding for list
|
||||
overflow: hidden;
|
||||
|
||||
.section-title { margin: 24px 24px 10px; }
|
||||
|
||||
.product-item {
|
||||
display: flex;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
|
||||
.p-img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 8px;
|
||||
background: #000;
|
||||
margin-right: 20px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.p-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.p-name { font-size: 28px; color: #fff; font-weight: bold; }
|
||||
.p-desc { font-size: 24px; color: #888; }
|
||||
|
||||
.p-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.p-price { font-size: 30px; color: #00b96b; font-weight: bold; }
|
||||
.p-qty { font-size: 26px; color: #888; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
font-size: 28px;
|
||||
color: #888;
|
||||
|
||||
&.total {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 32px;
|
||||
.price { color: #00b96b; font-size: 40px; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
@@ -53,22 +156,36 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
padding: 10px 20px;
|
||||
border-top: 1px solid #eee;
|
||||
height: 110px;
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 30px;
|
||||
z-index: 100;
|
||||
|
||||
.total-label { font-size: 28px; color: #fff; margin-right: 20px; }
|
||||
.total-price { font-size: 40px; color: #00b96b; font-weight: bold; margin-right: 30px; }
|
||||
|
||||
.btn-submit {
|
||||
background: #00b96b;
|
||||
color: #fff;
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(135deg, #00b96b 0%, #00f0ff 100%);
|
||||
color: #000;
|
||||
border-radius: 40px;
|
||||
padding: 0 60px;
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
box-shadow: 0 0 20px rgba(0, 185, 107, 0.3);
|
||||
|
||||
&:active { transform: scale(0.98); }
|
||||
&.disabled {
|
||||
background: #333;
|
||||
color: #666;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
@@ -1,98 +1,220 @@
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import { View, Text, Image, ScrollView, Button } from '@tarojs/components'
|
||||
import Taro, { useRouter, useLoad } from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { getConfigDetail, createOrder } from '../../api'
|
||||
import { getSelectedItems, removeItem } from '../../utils/cart'
|
||||
import './checkout.scss'
|
||||
|
||||
export default function Checkout() {
|
||||
const router = useRouter()
|
||||
const { id, quantity } = router.params
|
||||
const [product, setProduct] = useState<any>(null)
|
||||
const params = router.params
|
||||
const [items, setItems] = useState<any[]>([])
|
||||
const [address, setAddress] = useState<any>(null)
|
||||
const [contact, setContact] = useState({ name: '', phone: '' })
|
||||
const [deliveryType, setDeliveryType] = useState<'delivery' | 'pickup'>('delivery')
|
||||
const [userAddress, setUserAddress] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const PICKUP_ADDRESS = {
|
||||
userName: '云南量迹科技有限公司',
|
||||
telNumber: '18585164448',
|
||||
provinceName: '云南省',
|
||||
cityName: '昆明市',
|
||||
countyName: '西山区',
|
||||
detailInfo: '永昌街道办事处云纺国际商厦 B 座 1406 号'
|
||||
}
|
||||
|
||||
useLoad(async () => {
|
||||
if (id) {
|
||||
const res = await getConfigDetail(Number(id))
|
||||
setProduct(res)
|
||||
if (params.from === 'cart') {
|
||||
const cartItems = getSelectedItems()
|
||||
if (cartItems.length === 0) {
|
||||
Taro.navigateBack()
|
||||
return
|
||||
}
|
||||
setItems(cartItems)
|
||||
setLoading(false)
|
||||
} else if (params.id) {
|
||||
try {
|
||||
const res = await getConfigDetail(params.id)
|
||||
setItems([{
|
||||
id: res.id,
|
||||
name: res.name,
|
||||
price: res.price,
|
||||
image: res.static_image_url || res.detail_image_url,
|
||||
quantity: Number(params.quantity) || 1,
|
||||
description: res.description
|
||||
}])
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
Taro.showToast({ title: '商品加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const chooseAddress = async () => {
|
||||
if (deliveryType === 'pickup') return
|
||||
try {
|
||||
const res = await Taro.chooseAddress()
|
||||
setAddress(res)
|
||||
setContact({ name: res.userName, phone: res.telNumber })
|
||||
} catch (e) {
|
||||
Taro.showToast({ title: '需要授权获取地址', icon: 'none' })
|
||||
const res = await Taro.chooseAddress()
|
||||
setAddress(res)
|
||||
setUserAddress(res)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
// User cancelled or auth denied
|
||||
}
|
||||
}
|
||||
|
||||
const handleTypeChange = (type: 'delivery' | 'pickup') => {
|
||||
if (type === deliveryType) return
|
||||
setDeliveryType(type)
|
||||
if (type === 'pickup') {
|
||||
setAddress(PICKUP_ADDRESS)
|
||||
} else {
|
||||
setAddress(userAddress)
|
||||
}
|
||||
}
|
||||
|
||||
const totalPrice = useMemo(() => {
|
||||
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
||||
}, [items])
|
||||
|
||||
const submitOrder = async () => {
|
||||
if (!address) {
|
||||
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
|
||||
return
|
||||
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Taro.showLoading({ title: '提交中...' })
|
||||
|
||||
try {
|
||||
Taro.showLoading({ title: '正在下单...' })
|
||||
const orderPromises = items.map(item => {
|
||||
const orderData = {
|
||||
goodid: product.id,
|
||||
quantity: Number(quantity || 1),
|
||||
customer_name: contact.name,
|
||||
phone_number: contact.phone,
|
||||
shipping_address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}`,
|
||||
// ref_code: Taro.getStorageSync('ref_code')
|
||||
}
|
||||
|
||||
const res = await createOrder(orderData)
|
||||
Taro.hideLoading()
|
||||
|
||||
if (res.order_id) {
|
||||
Taro.redirectTo({ url: `/pages/order/payment?id=${res.order_id}` })
|
||||
goodid: item.id,
|
||||
quantity: item.quantity,
|
||||
customer_name: address.userName,
|
||||
phone_number: address.telNumber,
|
||||
shipping_address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}`
|
||||
}
|
||||
return createOrder(orderData)
|
||||
})
|
||||
|
||||
const results = await Promise.all(orderPromises)
|
||||
|
||||
// If from cart, remove bought items
|
||||
if (params.from === 'cart') {
|
||||
items.forEach(item => removeItem(item.id))
|
||||
}
|
||||
|
||||
Taro.hideLoading()
|
||||
|
||||
if (results.length === 1) {
|
||||
// Single order, go to payment
|
||||
const orderId = results[0].order_id
|
||||
Taro.redirectTo({
|
||||
url: `/pages/order/payment?id=${orderId}`
|
||||
})
|
||||
} else {
|
||||
// Multiple orders
|
||||
Taro.showModal({
|
||||
title: '下单成功',
|
||||
content: `成功创建 ${results.length} 个订单,请前往订单列表支付`,
|
||||
showCancel: false,
|
||||
confirmText: '去支付',
|
||||
success: () => {
|
||||
Taro.redirectTo({ url: '/pages/order/list' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Taro.hideLoading()
|
||||
console.error(err)
|
||||
Taro.hideLoading()
|
||||
console.error(err)
|
||||
Taro.showToast({ title: '下单失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
if (!product) return <View>Loading...</View>
|
||||
if (loading) return <View className='page-container'><View className='section'><Text>Loading...</Text></View></View>
|
||||
|
||||
return (
|
||||
<View className='page-container'>
|
||||
<View className='page-container'>
|
||||
<ScrollView scrollY style={{height: 'calc(100vh - 120px)'}}>
|
||||
{/* Delivery Type Section */}
|
||||
<View className='section delivery-type-section'>
|
||||
<View
|
||||
className={`type-item ${deliveryType === 'delivery' ? 'active' : ''}`}
|
||||
onClick={() => handleTypeChange('delivery')}
|
||||
>
|
||||
快递配送
|
||||
</View>
|
||||
<View
|
||||
className={`type-item ${deliveryType === 'pickup' ? 'active' : ''}`}
|
||||
onClick={() => handleTypeChange('pickup')}
|
||||
>
|
||||
门店自提
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Address Section */}
|
||||
<View className='section address-section' onClick={chooseAddress}>
|
||||
{address ? (
|
||||
<View>
|
||||
<View className='row'>
|
||||
<Text className='name'>{contact.name}</Text>
|
||||
<Text className='phone'>{contact.phone}</Text>
|
||||
<View className='address-info'>
|
||||
<View className='user-info'>
|
||||
<Text>{address.userName}</Text>
|
||||
<Text className='phone'>{address.telNumber}</Text>
|
||||
</View>
|
||||
<View className='address-text'>
|
||||
{address.provinceName}{address.cityName}{address.countyName}{address.detailInfo}
|
||||
</View>
|
||||
<Text className='addr'>{address.provinceName}{address.cityName}{address.countyName}{address.detailInfo}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='placeholder-container'>
|
||||
<Text className='placeholder'>+ 选择收货地址</Text>
|
||||
<View className='address-info'>
|
||||
<Text className='placeholder'>+ 添加收货地址</Text>
|
||||
</View>
|
||||
)}
|
||||
{deliveryType === 'delivery' && <Text className='arrow'>›</Text>}
|
||||
</View>
|
||||
|
||||
{/* Products Section */}
|
||||
<View className='section product-section'>
|
||||
<Text className='p-name'>{product.name}</Text>
|
||||
<View className='row'>
|
||||
<Text className='p-price'>¥{product.price}</Text>
|
||||
<Text className='p-qty'>x {quantity}</Text>
|
||||
</View>
|
||||
<View className='divider' />
|
||||
<View className='row total-row'>
|
||||
<Text>合计</Text>
|
||||
<Text className='total-price'>¥{(product.price * (Number(quantity) || 1)).toFixed(2)}</Text>
|
||||
</View>
|
||||
<Text className='section-title'>商品信息</Text>
|
||||
{items.map((item, idx) => (
|
||||
<View key={idx} className='product-item'>
|
||||
<Image src={item.image} className='p-img' mode='aspectFill' />
|
||||
<View className='p-info'>
|
||||
<Text className='p-name'>{item.name}</Text>
|
||||
<Text className='p-desc'>{item.description}</Text>
|
||||
<View className='p-meta'>
|
||||
<Text className='p-price'>¥{item.price}</Text>
|
||||
<Text className='p-qty'>x{item.quantity}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View className='bottom-bar safe-area-bottom'>
|
||||
<Button className='btn-submit' onClick={submitOrder}>提交订单</Button>
|
||||
{/* Summary Section */}
|
||||
<View className='section summary-section'>
|
||||
<View className='row'>
|
||||
<Text>商品总价</Text>
|
||||
<Text>¥{totalPrice}</Text>
|
||||
</View>
|
||||
<View className='row'>
|
||||
<Text>运费</Text>
|
||||
<Text>¥0</Text>
|
||||
</View>
|
||||
<View className='row total'>
|
||||
<Text>合计</Text>
|
||||
<Text className='price'>¥{totalPrice}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<View className='bottom-bar'>
|
||||
<Text className='total-label'>共{items.length}件</Text>
|
||||
<Text className='total-price'>¥{totalPrice}</Text>
|
||||
<Button className='btn-submit' onClick={submitOrder}>提交订单</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -128,59 +128,158 @@
|
||||
.process-section {
|
||||
margin-top: 60px;
|
||||
padding: 40px 20px;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,185,107,0.05) 100%);
|
||||
border-radius: 30px;
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Background Tech Grid
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 185, 107, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 185, 107, 0.03) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 40px;
|
||||
margin-bottom: 60px;
|
||||
display: block;
|
||||
text-shadow: 0 0 10px rgba(0, 185, 107, 0.5);
|
||||
text-shadow: 0 0 15px rgba(0, 185, 107, 0.8);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
letter-spacing: 2px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
background: #00b96b;
|
||||
margin: 15px auto 0;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 10px #00b96b;
|
||||
}
|
||||
}
|
||||
|
||||
.process-steps {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 0 20px;
|
||||
|
||||
// Vertical connecting line
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
bottom: 20px;
|
||||
left: 60px; // Center of the icon (40px + padding)
|
||||
width: 2px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
// Moving signal on the line
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 60px;
|
||||
width: 2px;
|
||||
height: 100px;
|
||||
background: linear-gradient(to bottom, transparent, #00b96b, transparent);
|
||||
animation: signalFlow 3s infinite linear;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.step-item {
|
||||
width: 48%; // 2 columns
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 40px;
|
||||
position: relative;
|
||||
|
||||
&:last-child { margin-bottom: 0; }
|
||||
|
||||
.step-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(0, 185, 107, 0.3);
|
||||
border-radius: 20px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid rgba(0, 185, 107, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 15px;
|
||||
color: #00b96b;
|
||||
font-size: 32px;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin-right: 30px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-shadow: 0 0 15px rgba(0, 185, 107, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
// Pulse effect for icon
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -5px; bottom: -5px; left: -5px; right: -5px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(0, 185, 107, 0.3);
|
||||
animation: pulseBorder 2s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.step-title {
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
color: #666;
|
||||
font-size: 24px;
|
||||
// Content Card
|
||||
.step-content-wrapper {
|
||||
flex: 1;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-left: 4px solid #00b96b;
|
||||
padding: 20px 24px;
|
||||
border-radius: 0 16px 16px 0;
|
||||
backdrop-filter: blur(5px);
|
||||
transform: translateX(0);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.step-title {
|
||||
color: #fff;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
text-shadow: 0 0 5px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
color: #888;
|
||||
font-size: 24px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes signalFlow {
|
||||
0% { top: 0; opacity: 0; }
|
||||
20% { opacity: 1; }
|
||||
80% { opacity: 1; }
|
||||
100% { top: 100%; opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes pulseBorder {
|
||||
0% { transform: scale(1); opacity: 0.5; }
|
||||
100% { transform: scale(1.15); opacity: 0; }
|
||||
}
|
||||
|
||||
@@ -91,8 +91,10 @@ export default function ServicesIndex() {
|
||||
].map((step) => (
|
||||
<View key={step.id} className='step-item'>
|
||||
<View className='step-icon'><Text>{step.id}</Text></View>
|
||||
<Text className='step-title'>{step.title}</Text>
|
||||
<Text className='step-desc'>{step.desc}</Text>
|
||||
<View className='step-content-wrapper'>
|
||||
<Text className='step-title'>{step.title}</Text>
|
||||
<Text className='step-desc'>{step.desc}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -1,53 +1,207 @@
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f7f8fa;
|
||||
background-color: #050505;
|
||||
color: #fff;
|
||||
padding: 30px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
padding: 40px 20px;
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(0, 185, 107, 0.4); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(0, 185, 107, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(0, 185, 107, 0); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%);
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 30px;
|
||||
margin-right: 15px;
|
||||
background: #eee;
|
||||
.card-bg-effect {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -20%;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: radial-gradient(circle, rgba(0, 185, 107, 0.2) 0%, transparent 70%);
|
||||
filter: blur(40px);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
margin-right: 30px;
|
||||
z-index: 1;
|
||||
|
||||
.avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 60px;
|
||||
border: 2px solid rgba(0, 185, 107, 0.5);
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.online-dot {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #00b96b;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #111;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.info-col {
|
||||
flex: 1;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.nickname {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 8px;
|
||||
text-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.uid {
|
||||
font-size: 24px;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
background: rgba(0, 185, 107, 0.2);
|
||||
border: 1px solid #00b96b;
|
||||
color: #00b96b;
|
||||
font-size: 24px;
|
||||
border-radius: 30px;
|
||||
padding: 0 30px;
|
||||
height: 60px;
|
||||
line-height: 58px;
|
||||
margin: 0;
|
||||
width: fit-content;
|
||||
|
||||
&:active { background: rgba(0, 185, 107, 0.3); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
background: #fff;
|
||||
|
||||
.item {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
.stats-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
margin-bottom: 30px;
|
||||
padding: 0 10px;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
|
||||
.arrow { color: #ccc; }
|
||||
|
||||
.btn-contact {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.stat-val { font-size: 36px; font-weight: bold; color: #fff; margin-bottom: 5px; }
|
||||
.stat-lbl { font-size: 24px; color: #666; }
|
||||
}
|
||||
}
|
||||
|
||||
.service-container {
|
||||
padding-bottom: 40px;
|
||||
|
||||
.service-group {
|
||||
margin-bottom: 40px;
|
||||
|
||||
.group-title {
|
||||
display: block;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 20px;
|
||||
padding-left: 10px;
|
||||
border-left: 4px solid #00b96b;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.grid-layout {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
|
||||
.grid-item {
|
||||
width: calc(33.33% - 14px); // 3 items per row, accounting for gap
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
padding: 30px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.icon-box {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 185, 107, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.icon { font-size: 40px; }
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 26px;
|
||||
color: #ddd;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contact-overlay {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.version-info {
|
||||
margin-top: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
text {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,33 +13,99 @@ export default function UserIndex() {
|
||||
|
||||
const goOrders = () => Taro.navigateTo({ url: '/pages/order/list' })
|
||||
const goDistributor = () => Taro.navigateTo({ url: '/subpackages/distributor/index' })
|
||||
const goInvite = () => Taro.navigateTo({ url: '/subpackages/distributor/invite' })
|
||||
const goWithdraw = () => Taro.navigateTo({ url: '/subpackages/distributor/withdraw' })
|
||||
|
||||
const handleAddress = async () => {
|
||||
try { await Taro.chooseAddress() } catch(e) {}
|
||||
}
|
||||
|
||||
const login = () => {
|
||||
// Trigger login again if needed
|
||||
Taro.reLaunch({ url: '/pages/index/index' })
|
||||
}
|
||||
|
||||
const serviceGroups = [
|
||||
{
|
||||
title: '基础服务',
|
||||
items: [
|
||||
{ title: '我的订单', icon: '📦', action: goOrders },
|
||||
{ title: '地址管理', icon: '📍', action: handleAddress },
|
||||
{ title: '新增地址', icon: '📝', action: handleAddress },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '分销中心',
|
||||
items: [
|
||||
{ title: '分销首页', icon: '⚡', action: goDistributor },
|
||||
{ title: '推广邀请', icon: '🤝', action: goInvite },
|
||||
{ title: '佣金提现', icon: '💰', action: goWithdraw },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '其他',
|
||||
items: [
|
||||
{ title: '联系客服', icon: '🎧', isContact: true }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const stats = [
|
||||
{ label: '余额', value: '0.00' },
|
||||
{ label: '积分', value: '0' },
|
||||
{ label: '优惠券', value: '0' }
|
||||
]
|
||||
|
||||
return (
|
||||
<View className='page-container'>
|
||||
<View className='header'>
|
||||
<Image src={userInfo?.avatar_url || 'https://via.placeholder.com/100'} className='avatar' />
|
||||
<Text className='nickname'>{userInfo?.nickname || '未登录'}</Text>
|
||||
{!userInfo && <Button size='mini' onClick={login}>点击登录</Button>}
|
||||
{/* Profile Card */}
|
||||
<View className='profile-card'>
|
||||
<View className='avatar-container'>
|
||||
<Image src={userInfo?.avatar_url || 'https://via.placeholder.com/150/00b96b/FFFFFF?text=USER'} className='avatar' />
|
||||
{userInfo && <View className='online-dot' />}
|
||||
</View>
|
||||
<View className='info-col'>
|
||||
<Text className='nickname'>{userInfo?.nickname || '未登录用户'}</Text>
|
||||
<Text className='uid'>ID: {userInfo ? '888888' : '----'}</Text>
|
||||
{!userInfo && (
|
||||
<Button className='btn-login' onClick={login}>立即登录 / 注册</Button>
|
||||
)}
|
||||
</View>
|
||||
<View className='card-bg-effect' />
|
||||
</View>
|
||||
|
||||
<View className='menu'>
|
||||
<View className='item' onClick={goOrders}>
|
||||
<Text>我的订单</Text>
|
||||
<Text className='arrow'>></Text>
|
||||
</View>
|
||||
<View className='item' onClick={goDistributor}>
|
||||
<Text>分销中心</Text>
|
||||
<Text className='arrow'>></Text>
|
||||
</View>
|
||||
<View className='item'>
|
||||
<Text>联系客服</Text>
|
||||
<Button openType='contact' className='btn-contact' />
|
||||
<Text className='arrow'>></Text>
|
||||
</View>
|
||||
{/* Stats Row */}
|
||||
<View className='stats-row'>
|
||||
{stats.map((item, idx) => (
|
||||
<View key={idx} className='stat-item'>
|
||||
<Text className='stat-val'>{item.value}</Text>
|
||||
<Text className='stat-lbl'>{item.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Service Groups */}
|
||||
<View className='service-container'>
|
||||
{serviceGroups.map((group, gIdx) => (
|
||||
<View key={gIdx} className='service-group'>
|
||||
<Text className='group-title'>{group.title}</Text>
|
||||
<View className='grid-layout'>
|
||||
{group.items.map((item, idx) => (
|
||||
<View key={idx} className='grid-item' onClick={item.action}>
|
||||
<View className='icon-box'>
|
||||
<Text className='icon'>{item.icon}</Text>
|
||||
</View>
|
||||
<Text className='item-title'>{item.title}</Text>
|
||||
{item.isContact && <Button openType='contact' className='contact-overlay' />}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View className='version-info'>
|
||||
<Text>Quant Speed Market v1.0.0</Text>
|
||||
<Text>Powered by Taro & React</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@ export default function DistributorIndex() {
|
||||
|
||||
const goInvite = () => Taro.navigateTo({ url: '/subpackages/distributor/invite' })
|
||||
const goWithdraw = () => Taro.navigateTo({ url: '/subpackages/distributor/withdraw' })
|
||||
const showComingSoon = () => Taro.showToast({ title: '功能开发中', icon: 'none' })
|
||||
|
||||
if (loading) return <View>Loading...</View>
|
||||
if (!info) return <View>Error</View>
|
||||
@@ -62,11 +63,11 @@ export default function DistributorIndex() {
|
||||
<Text>推广二维码</Text>
|
||||
<Text className='arrow'>></Text>
|
||||
</View>
|
||||
<View className='menu-item'>
|
||||
<View className='menu-item' onClick={showComingSoon}>
|
||||
<Text>我的团队</Text>
|
||||
<Text className='arrow'>></Text>
|
||||
</View>
|
||||
<View className='menu-item'>
|
||||
<View className='menu-item' onClick={showComingSoon}>
|
||||
<Text>提现记录</Text>
|
||||
<Text className='arrow'>></Text>
|
||||
</View>
|
||||
|
||||
90
miniprogram/src/utils/cart.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const CART_KEY = 'MARKET_CART'
|
||||
|
||||
export interface CartItem {
|
||||
id: number
|
||||
name: string
|
||||
price: number
|
||||
image: string
|
||||
quantity: number
|
||||
selected: boolean
|
||||
stock: number
|
||||
description: string
|
||||
}
|
||||
|
||||
export const getCart = (): CartItem[] => {
|
||||
return Taro.getStorageSync(CART_KEY) || []
|
||||
}
|
||||
|
||||
export const setCart = (cart: CartItem[]) => {
|
||||
Taro.setStorageSync(CART_KEY, cart)
|
||||
}
|
||||
|
||||
export const addToCart = (product: any, quantity: number = 1) => {
|
||||
const cart = getCart()
|
||||
const existing = cart.find(item => item.id === product.id)
|
||||
|
||||
if (existing) {
|
||||
existing.quantity += quantity
|
||||
if (existing.quantity > product.stock) existing.quantity = product.stock
|
||||
} else {
|
||||
cart.push({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.static_image_url || product.detail_image_url,
|
||||
quantity: quantity,
|
||||
selected: true,
|
||||
stock: product.stock,
|
||||
description: product.description
|
||||
})
|
||||
}
|
||||
|
||||
setCart(cart)
|
||||
Taro.showToast({ title: '已加入购物车', icon: 'success' })
|
||||
}
|
||||
|
||||
export const updateQuantity = (id: number, quantity: number) => {
|
||||
const cart = getCart()
|
||||
const item = cart.find(i => i.id === id)
|
||||
if (item) {
|
||||
item.quantity = quantity
|
||||
if (item.quantity > item.stock) item.quantity = item.stock
|
||||
if (item.quantity < 1) item.quantity = 1
|
||||
setCart(cart)
|
||||
}
|
||||
return cart
|
||||
}
|
||||
|
||||
export const removeItem = (id: number) => {
|
||||
const cart = getCart()
|
||||
const newCart = cart.filter(i => i.id !== id)
|
||||
setCart(newCart)
|
||||
return newCart
|
||||
}
|
||||
|
||||
export const toggleSelect = (id: number) => {
|
||||
const cart = getCart()
|
||||
const item = cart.find(i => i.id === id)
|
||||
if (item) {
|
||||
item.selected = !item.selected
|
||||
setCart(cart)
|
||||
}
|
||||
return cart
|
||||
}
|
||||
|
||||
export const toggleSelectAll = (selected: boolean) => {
|
||||
const cart = getCart()
|
||||
cart.forEach(item => item.selected = selected)
|
||||
setCart(cart)
|
||||
return cart
|
||||
}
|
||||
|
||||
export const getSelectedItems = () => {
|
||||
return getCart().filter(item => item.selected)
|
||||
}
|
||||
|
||||
export const getCartCount = () => {
|
||||
return getCart().reduce((sum, item) => sum + item.quantity, 0)
|
||||
}
|
||||