This commit is contained in:
jeremygan2021
2026-02-11 03:00:38 +08:00
parent c3b4373c94
commit 96d5598fb5
57 changed files with 2239 additions and 577 deletions

View File

@@ -12,7 +12,7 @@ links = [
"admin:shop_distributor_changelist", "admin:shop_distributor_changelist",
"admin:shop_esp32config_changelist", "admin:shop_esp32config_changelist",
"admin:shop_service_changelist", "admin:shop_service_changelist",
"admin:shop_arservice_changelist", "admin:shop_vbcourse_changelist",
"admin:shop_order_changelist", "admin:shop_order_changelist",
"admin:shop_serviceorder_changelist", "admin:shop_serviceorder_changelist",
"admin:shop_withdrawal_changelist", "admin:shop_withdrawal_changelist",

View File

@@ -232,9 +232,9 @@ UNFOLD = {
"link": reverse_lazy("admin:shop_service_changelist"), "link": reverse_lazy("admin:shop_service_changelist"),
}, },
{ {
"title": "AR体验", "title": "VB课程",
"icon": "view_in_ar", "icon": "school",
"link": reverse_lazy("admin:shop_arservice_changelist"), "link": reverse_lazy("admin:shop_vbcourse_changelist"),
}, },
], ],
}, },

Binary file not shown.

View File

@@ -4,7 +4,7 @@ from django.db.models import Sum
from django import forms from django import forms
from unfold.admin import ModelAdmin, TabularInline from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import display 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 import qrcode
from io import BytesIO from io import BytesIO
import base64 import base64
@@ -19,11 +19,11 @@ class ExternalUploadWidget(forms.URLInput):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.upload_url = upload_url self.upload_url = upload_url
self.attrs.update({ self.attrs.update({
'class': 'upload-url-input', 'class': 'upload-url-input vTextField',
'data-upload-url': upload_url, 'data-upload-url': upload_url,
'data-accept': accept, 'data-accept': accept,
'readonly': 'readonly', 'placeholder': '上传文件后自动生成URL',
'placeholder': '上传文件后自动生成URL' 'style': 'width: 100%;'
}) })
class Media: class Media:
@@ -141,18 +141,26 @@ class ServiceOrderAdmin(ModelAdmin):
}), }),
) )
@admin.register(ARService) @admin.register(VBCourse)
class ARServiceAdmin(ModelAdmin): class VBCourseAdmin(ModelAdmin):
list_display = ('title', 'created_at') list_display = ('title', 'course_type', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at')
search_fields = ('title', 'description') search_fields = ('title', 'description', 'instructor', 'tag')
list_filter = ('course_type', 'instructor', 'tag')
fieldsets = ( fieldsets = (
('基本信息', { ('基本信息', {
'fields': ('title', 'description') 'fields': ('title', 'description', 'course_type', 'tag')
}), }),
('封面/长图', { ('课程详情', {
'fields': ('instructor', 'duration', 'lesson_count')
}),
('封面', {
'fields': ('cover_image', 'cover_image_url'), 'fields': ('cover_image', 'cover_image_url'),
'description': '图片上传和URL二选一优先使用URL' 'description': '图片上传和URL二选一优先使用URL'
}), }),
('详情页长图', {
'fields': ('detail_image', 'detail_image_url'),
'description': '图片上传和URL二选一优先使用URL'
}),
) )
@admin.register(Salesperson) @admin.register(Salesperson)

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

View File

@@ -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='标签'),
),
]

View 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='课程类型'),
),
]

View File

@@ -312,19 +312,36 @@ class ServiceOrder(models.Model):
verbose_name_plural = "服务订单列表" verbose_name_plural = "服务订单列表"
class ARService(models.Model): class VBCourse(models.Model):
""" """
AR体验服务模型 VB Coding 课程模型
""" """
title = models.CharField(max_length=100, verbose_name="体验名称") COURSE_TYPE_CHOICES = (
description = models.TextField(verbose_name="简介") ('software', '软件课程'),
cover_image = models.ImageField(upload_to='ar/covers/', blank=True, null=True, verbose_name="封面/长图 (上传)") ('hardware', '硬件课程'),
cover_image_url = models.URLField(blank=True, null=True, verbose_name="封面/长图 (URL)") ('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="创建时间") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
def __str__(self): def __str__(self):
return self.title return self.title
class Meta: class Meta:
verbose_name = "AR体验" verbose_name = "VB课程"
verbose_name_plural = "AR体验管理" verbose_name_plural = "VB课程管理"

View File

@@ -1,5 +1,5 @@
from rest_framework import serializers from rest_framework import serializers
from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal from .models import ESP32Config, Order, Salesperson, Service, VBCourse, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal
class WeChatUserSerializer(serializers.ModelSerializer): class WeChatUserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@@ -101,14 +101,16 @@ class ServiceOrderSerializer(serializers.ModelSerializer):
return super().create(validated_data) return super().create(validated_data)
class ARServiceSerializer(serializers.ModelSerializer): class VBCourseSerializer(serializers.ModelSerializer):
""" """
AR服务序列化器 VB课程序列化器
""" """
display_cover_image = serializers.SerializerMethodField() 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: class Meta:
model = ARService model = VBCourse
fields = '__all__' fields = '__all__'
def get_display_cover_image(self, obj): def get_display_cover_image(self, obj):
@@ -118,6 +120,13 @@ class ARServiceSerializer(serializers.ModelSerializer):
return obj.cover_image.url return obj.cover_image.url
return None 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): class ESP32ConfigSerializer(serializers.ModelSerializer):
""" """
ESP32配置序列化器 ESP32配置序列化器

View File

@@ -2,7 +2,7 @@ from django.urls import path, include, re_path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .views import ( from .views import (
ESP32ConfigViewSet, OrderViewSet, order_check_view, ESP32ConfigViewSet, OrderViewSet, order_check_view,
ServiceViewSet, ARServiceViewSet, ServiceOrderViewSet, ServiceViewSet, VBCourseViewSet, ServiceOrderViewSet,
payment_finish, pay, send_sms_code, wechat_login, update_user_info, DistributorViewSet 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'configs', ESP32ConfigViewSet)
router.register(r'orders', OrderViewSet) router.register(r'orders', OrderViewSet)
router.register(r'services', ServiceViewSet) router.register(r'services', ServiceViewSet)
router.register(r'ar', ARServiceViewSet) router.register(r'courses', VBCourseViewSet)
router.register(r'service-orders', ServiceOrderViewSet) router.register(r'service-orders', ServiceOrderViewSet)
router.register(r'distributor', DistributorViewSet, basename='distributor') router.register(r'distributor', DistributorViewSet, basename='distributor')

View File

@@ -5,8 +5,8 @@ from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse from django.http import HttpResponse
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiExample 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 .models import ESP32Config, Order, WeChatPayConfig, Service, VBCourse, ServiceOrder, Salesperson, CommissionLog, WeChatUser, Distributor, Withdrawal
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, VBCourseSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.contrib.auth.models import User from django.contrib.auth.models import User
from wechatpayv3 import WeChatPay, WeChatPayType from wechatpayv3 import WeChatPay, WeChatPayType
@@ -508,15 +508,15 @@ def payment_finish(request):
return HttpResponse(str(e), status=500) return HttpResponse(str(e), status=500)
@extend_schema_view( @extend_schema_view(
list=extend_schema(summary="获取AR服务列表", description="获取所有可用的AR服务"), list=extend_schema(summary="获取VB课程列表", description="获取所有可用的VB课程"),
retrieve=extend_schema(summary="获取AR服务详情", description="获取指定AR服务的详细信息") 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') queryset = VBCourse.objects.all().order_by('-created_at')
serializer_class = ARServiceSerializer serializer_class = VBCourseSerializer
def order_check_view(request): def order_check_view(request):
""" """

View File

@@ -6,7 +6,7 @@ import ProductDetail from './pages/ProductDetail';
import Payment from './pages/Payment'; import Payment from './pages/Payment';
import AIServices from './pages/AIServices'; import AIServices from './pages/AIServices';
import ServiceDetail from './pages/ServiceDetail'; import ServiceDetail from './pages/ServiceDetail';
import ARExperience from './pages/ARExperience'; import VBCourses from './pages/VBCourses';
import MyOrders from './pages/MyOrders'; import MyOrders from './pages/MyOrders';
import 'antd/dist/reset.css'; import 'antd/dist/reset.css';
import './App.css'; import './App.css';
@@ -19,7 +19,7 @@ function App() {
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/services" element={<AIServices />} /> <Route path="/services" element={<AIServices />} />
<Route path="/services/:id" element={<ServiceDetail />} /> <Route path="/services/:id" element={<ServiceDetail />} />
<Route path="/ar" element={<ARExperience />} /> <Route path="/courses" element={<VBCourses />} />
<Route path="/my-orders" element={<MyOrders />} /> <Route path="/my-orders" element={<MyOrders />} />
<Route path="/product/:id" element={<ProductDetail />} /> <Route path="/product/:id" element={<ProductDetail />} />
<Route path="/payment/:orderId" element={<Payment />} /> <Route path="/payment/:orderId" element={<Payment />} />

View File

@@ -19,7 +19,7 @@ export const confirmPayment = (orderId) => api.post(`/orders/${orderId}/confirm_
export const getServices = () => api.get('/services/'); export const getServices = () => api.get('/services/');
export const getServiceDetail = (id) => api.get(`/services/${id}/`); export const getServiceDetail = (id) => api.get(`/services/${id}/`);
export const createServiceOrder = (data) => api.post('/service-orders/', data); export const 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 sendSms = (data) => api.post('/auth/send-sms/', data);
export const queryMyOrders = (data) => api.post('/orders/my_orders/', data); export const queryMyOrders = (data) => api.post('/orders/my_orders/', data);

View File

@@ -34,9 +34,9 @@ const Layout = ({ children }) => {
label: 'AI 服务', label: 'AI 服务',
}, },
{ {
key: '/ar', key: '/courses',
icon: <EyeOutlined />, icon: <EyeOutlined />,
label: 'AR 体验', label: 'VB 课程',
}, },
{ {
key: '/my-orders', key: '/my-orders',

View File

@@ -1,28 +1,27 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Button, Typography, Spin, Row, Col, Empty } from 'antd'; import { Button, Typography, Spin, Row, Col, Empty, Tag } from 'antd';
import { ScanOutlined } from '@ant-design/icons'; import { ReadOutlined, ClockCircleOutlined, UserOutlined, BookOutlined } from '@ant-design/icons';
import { getARServices } from '../api'; import { getVBCourses } from '../api';
const { Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;
const ARExperience = () => { const VBCourses = () => {
const [scanning, setScanning] = useState(true); const [courses, setCourses] = useState([]);
const [arServices, setArServices] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
const fetchAR = async () => { const fetchCourses = async () => {
try { try {
const res = await getARServices(); const res = await getVBCourses();
setArServices(res.data); setCourses(res.data);
} catch (error) { } catch (error) {
console.error("Failed to fetch AR services:", error); console.error("Failed to fetch VB Courses:", error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
} }
fetchAR(); fetchCourses();
}, []); }, []);
if (loading) return <div style={{ textAlign: 'center', padding: 100 }}><Spin size="large" /></div>; 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={{ padding: '40px 0', minHeight: '80vh', position: 'relative' }}>
<div style={{ textAlign: 'center', marginBottom: 60, position: 'relative', zIndex: 2 }}> <div style={{ textAlign: 'center', marginBottom: 60, position: 'relative', zIndex: 2 }}>
<Title level={1} style={{ color: '#fff', letterSpacing: 4 }}> <Title level={1} style={{ color: '#fff', letterSpacing: 4 }}>
AR <span style={{ color: '#00f0ff' }}>UNIVERSE</span> VB <span style={{ color: '#00f0ff' }}>CODING COURSES</span>
</Title> </Title>
<Paragraph style={{ color: '#aaa', fontSize: 18, maxWidth: 600, margin: '0 auto' }}> <Paragraph style={{ color: '#aaa', fontSize: 18, maxWidth: 600, margin: '0 auto' }}>
探索全息增强现实体验请佩戴您的设备或使用移动端摄像头扫描空间 探索 Vibe Coding 软件与硬件课程开启您的编程之旅
</Paragraph> </Paragraph>
</div> </div>
{arServices.length === 0 ? ( {courses.length === 0 ? (
<div style={{ textAlign: 'center', marginTop: 100, zIndex: 2, position: 'relative' }}> <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> </div>
) : ( ) : (
<Row gutter={[32, 32]} justify="center" style={{ padding: '0 20px', position: 'relative', zIndex: 2 }}> <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}> <Col xs={24} md={12} lg={8} key={item.id}>
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
@@ -57,20 +56,35 @@ const ARExperience = () => {
border: '1px solid rgba(0,240,255,0.2)', border: '1px solid rgba(0,240,255,0.2)',
borderRadius: 12, borderRadius: 12,
overflow: 'hidden', 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 ? ( {item.display_cover_image ? (
<img src={item.display_cover_image} alt={item.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> <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>
<div style={{ padding: 20 }}> <div style={{ padding: 20, flex: 1, display: 'flex', flexDirection: 'column' }}>
<h3 style={{ color: '#fff', fontSize: 20 }}>{item.title}</h3> <h3 style={{ color: '#fff', fontSize: 20, marginBottom: 10 }}>{item.title}</h3>
<p style={{ color: '#888', marginBottom: 20, minHeight: 44 }}>{item.description}</p> <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 type="primary" block ghost style={{ borderColor: '#00f0ff', color: '#00f0ff' }}>
启动体验 开始学习
</Button> </Button>
</div> </div>
</div> </div>
@@ -108,4 +122,4 @@ const ARExperience = () => {
); );
}; };
export default ARExperience; export default VBCourses;

View File

@@ -22,7 +22,7 @@ const config = {
framework: 'react', framework: 'react',
compiler: 'webpack5', compiler: 'webpack5',
cache: { cache: {
enable: false // Disable cache to avoid potential issues enable: true // Enable cache for better build performance
}, },
mini: { mini: {
postcss: { postcss: {

View File

@@ -15,15 +15,18 @@ export const getServices = () => request({ url: '/services/' })
export const getServiceDetail = (id: number) => request({ url: `/services/${id}/` }) export const getServiceDetail = (id: number) => request({ url: `/services/${id}/` })
export const createServiceOrder = (data: any) => request({ url: '/service-orders/', method: 'POST', data }) export const createServiceOrder = (data: any) => request({ url: '/service-orders/', method: 'POST', data })
// AR Services // VB Courses
export const getARServices = () => request({ url: '/ar/' }) export const getVBCourses = () => request({ url: '/courses/' })
export const getARServiceDetail = (id: number) => request({ url: `/ar/${id}/` }) export const getVBCourseDetail = (id: number) => request({ url: `/courses/${id}/` })
// Distributor // Distributor
export const distributorRegister = (data: any) => request({ url: '/distributor/register/', method: 'POST', data }) export const distributorRegister = (data: any) => request({ url: '/distributor/register/', method: 'POST', data })
export const distributorInfo = () => request({ url: '/distributor/info/' }) export const distributorInfo = () => request({ url: '/distributor/info/' })
export const distributorInvite = () => request({ url: '/distributor/invite/', method: 'POST' }) export const distributorInvite = () => request({ url: '/distributor/invite/', method: 'POST' })
export const distributorWithdraw = (amount: number) => request({ url: '/distributor/withdraw/', method: 'POST', data: { amount } }) 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 // User
export const updateUserInfo = (data: any) => request({ url: '/wechat/update/', method: 'POST', data }) export const updateUserInfo = (data: any) => request({ url: '/wechat/update/', method: 'POST', data })

View File

@@ -3,8 +3,8 @@ export default defineAppConfig({
'pages/index/index', 'pages/index/index',
'pages/services/index', 'pages/services/index',
'pages/services/detail', 'pages/services/detail',
'pages/ar/index', 'pages/courses/index',
'pages/ar/detail', 'pages/courses/detail',
'pages/goods/detail', 'pages/goods/detail',
'pages/cart/cart', 'pages/cart/cart',
'pages/order/checkout', 'pages/order/checkout',
@@ -25,14 +25,15 @@ export default defineAppConfig({
], ],
window: { window: {
backgroundTextStyle: 'light', backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff', navigationBarBackgroundColor: '#000000',
navigationBarTitleText: 'Quant Speed Market', navigationBarTitleText: 'Quant Speed Market',
navigationBarTextStyle: 'black' navigationBarTextStyle: 'white'
}, },
tabBar: { tabBar: {
color: "#999", color: "#666666",
selectedColor: "#333", selectedColor: "#00b96b",
backgroundColor: "#fff", backgroundColor: "#000000",
borderStyle: "black",
list: [ list: [
{ {
pagePath: "pages/index/index", pagePath: "pages/index/index",
@@ -43,13 +44,19 @@ export default defineAppConfig({
{ {
pagePath: "pages/services/index", pagePath: "pages/services/index",
text: "AI服务", text: "AI服务",
iconPath: "./assets/cart.png", // Using cart icon as placeholder if no other icon available iconPath: "./assets/AI_service.png",
selectedIconPath: "./assets/cart_active.png" selectedIconPath: "./assets/AI_service_active.png"
}, },
{ {
pagePath: "pages/ar/index", pagePath: "pages/courses/index",
text: "AR体验", text: "VB课程",
iconPath: "./assets/cart.png", // Placeholder iconPath: "./assets/VR.png",
selectedIconPath: "./assets/VR_active.png"
},
{
pagePath: "pages/cart/cart",
text: "购物车",
iconPath: "./assets/cart.png",
selectedIconPath: "./assets/cart_active.png" selectedIconPath: "./assets/cart_active.png"
}, },
{ {

View File

@@ -1,8 +1,18 @@
page { 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, font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica,
Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei',
sans-serif; sans-serif;
color: var(--text-main);
} }
.container { .container {

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 B

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 B

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 B

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,6 +1,104 @@
<svg width="66" height="66" viewBox="0 0 66 66" fill="none" xmlns="http://www.w3.org/2000/svg"> <?xml version="1.0" standalone="no"?>
<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"/> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
<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"/> "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<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"/> <svg version="1.0" xmlns="http://www.w3.org/2000/svg"
<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"/> 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> </svg>

Before

Width:  |  Height:  |  Size: 526 B

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 B

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -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>
)
}

View File

@@ -1,8 +1,214 @@
.page-container { .page-container {
min-height: 100vh; min-height: 100vh;
background-color: #f7f8fa; background-color: #050505;
display: flex; color: #fff;
justify-content: center; padding-bottom: 120px;
align-items: center; 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; }

View File

@@ -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' import './cart.scss'
export default function Cart() { 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 ( return (
<View className='page-container'> <View className='page-container'>
<View className='empty'> {cartItems.length === 0 ? (
<Text>线</Text> <View className='empty-state'>
</View> <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> </View>
) )
} }

View File

@@ -13,6 +13,28 @@
display: block; 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 { .desc {
color: #aaa; color: #aaa;
font-size: 28px; font-size: 28px;
@@ -20,7 +42,7 @@
display: block; display: block;
} }
.ar-placeholder { .course-placeholder {
width: 100%; width: 100%;
height: 500px; height: 500px;
background: #111; background: #111;

View 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>
)
}

View File

@@ -68,6 +68,33 @@
font-size: 80px; font-size: 80px;
font-weight: bold; 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 { .content {
@@ -80,6 +107,16 @@
display: block; display: block;
} }
.info-row {
display: flex;
gap: 20px;
margin-bottom: 10px;
.info-text {
color: #aaa;
font-size: 24px;
}
}
.item-desc { .item-desc {
color: #888; color: #888;
font-size: 26px; font-size: 26px;

View File

@@ -1,21 +1,21 @@
import { View, Text, Image, Button } from '@tarojs/components' import { View, Text, Image, Button } from '@tarojs/components'
import Taro, { useLoad } from '@tarojs/taro' import Taro, { useLoad } from '@tarojs/taro'
import { useState } from 'react' import { useState } from 'react'
import { getARServices } from '../../api' import { getVBCourses } from '../../api'
import './index.scss' import './index.scss'
export default function ARIndex() { export default function CourseIndex() {
const [arList, setArList] = useState<any[]>([]) const [courseList, setCourseList] = useState<any[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
useLoad(() => { useLoad(() => {
fetchAR() fetchCourses()
}) })
const fetchAR = async () => { const fetchCourses = async () => {
try { try {
const res: any = await getARServices() const res: any = await getVBCourses()
setArList(res.results || res) setCourseList(res.results || res)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
Taro.showToast({ title: '加载失败', icon: 'none' }) Taro.showToast({ title: '加载失败', icon: 'none' })
@@ -25,7 +25,7 @@ export default function ARIndex() {
} }
const goDetail = (id: number) => { 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> 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='bg-decoration' />
<View className='header'> <View className='header'>
<Text className='title'>AR <Text className='highlight'>UNIVERSE</Text></Text> <Text className='title'>VB <Text className='highlight'>COURSES</Text></Text>
<Text className='desc'></Text> <Text className='desc'> VB </Text>
</View> </View>
<View className='ar-grid'> <View className='ar-grid'>
{arList.length === 0 ? ( {courseList.length === 0 ? (
<View style={{ width: '100%', textAlign: 'center', color: '#666', marginTop: 50 }}> <View style={{ width: '100%', textAlign: 'center', color: '#666', marginTop: 50 }}>
<Text> AR </Text> <Text> VB </Text>
</View> </View>
) : ( ) : (
arList.map((item) => ( courseList.map((item) => (
<View key={item.id} className='ar-card' onClick={() => goDetail(item.id)}> <View key={item.id} className='ar-card' onClick={() => goDetail(item.id)}>
<View className='cover-box'> <View className='cover-box'>
{item.cover_image_url ? ( {item.cover_image_url ? (
<Image src={item.cover_image_url} className='cover-img' mode='aspectFill' /> <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>
<View className='content'> <View className='content'>
<Text className='item-title'>{item.title}</Text> <Text className='item-title'>{item.title}</Text>
<Text className='item-desc'>{item.description}</Text> <Text className='item-desc'>{item.description}</Text>
<Button className='btn-start'></Button> <Button className='btn-start'></Button>
</View> </View>
</View> </View>
)) ))

View File

@@ -1,8 +1,10 @@
.page-container { .page-container {
height: 100vh; min-height: 100vh;
background-color: #000; background-color: #050505;
color: #fff; color: #fff;
position: relative; position: relative;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
overflow-x: hidden;
} }
.loading-screen, .error-screen { .loading-screen, .error-screen {
@@ -10,147 +12,259 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #666; color: #00f0ff;
background: #000;
font-size: 28px;
letter-spacing: 2px;
} }
.content { .content {
height: 100vh; height: 100vh;
background: #000; position: relative;
z-index: 1;
padding-bottom: 200px; // Ensure scroll space for bottom bar
} }
.glass-panel { // Animations
background: rgba(255, 255, 255, 0.05); @keyframes fadeInUp {
backdrop-filter: blur(20px); from { opacity: 0; transform: translateY(40px); }
-webkit-backdrop-filter: blur(20px); to { opacity: 1; transform: translateY(0); }
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
} }
@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 { .hero-section {
position: relative; position: relative;
margin-bottom: 20px; margin-bottom: 40px;
animation: fadeInUp 0.8s ease-out;
.image-container { .image-container {
width: 100%; width: 100%;
min-height: 600px; min-height: 600px; // Slightly reduced to fit better
background: radial-gradient(circle at center, #1a1a1a, #000); background: radial-gradient(circle at center, rgba(0, 240, 255, 0.05) 0%, transparent 70%);
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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 { .hero-img {
width: 100%; width: 75%;
height: auto;
display: block; 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 { .placeholder-box {
.icon-bolt { font-size: 100px; } .icon-bolt { font-size: 150px; color: #00b96b; text-shadow: 0 0 30px rgba(0, 185, 107, 0.5); }
}
.hero-overlay {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 60%;
background: linear-gradient(to top, #000 10%, transparent);
} }
} }
.hero-content { .hero-content {
padding: 0 30px; padding: 0 40px;
margin-top: -100px; // Pull up over image margin-top: -40px;
position: relative; position: relative;
z-index: 2; z-index: 2;
.hero-title { .hero-title {
font-size: 48px; font-size: 60px;
font-weight: 900; font-weight: 900;
color: #fff; color: #fff;
display: block; display: block;
margin-bottom: 15px; margin-bottom: 24px;
text-shadow: 0 0 20px rgba(0,0,0,0.8); line-height: 1.1;
text-shadow: 0 0 20px rgba(0, 240, 255, 0.3);
letter-spacing: 1px;
} }
.hero-desc { .hero-desc {
font-size: 28px; font-size: 28px;
color: #ccc; color: rgba(255, 255, 255, 0.7);
line-height: 1.5; line-height: 1.6;
display: block; display: block;
margin-bottom: 25px; margin-bottom: 32px;
text-shadow: 0 0 10px rgba(0,0,0,0.8); font-weight: 300;
} }
.tags-row { .tags-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 15px; gap: 16px;
.tag { .tag {
padding: 8px 20px; padding: 10px 28px;
border-radius: 30px; border-radius: 4px; // Techy sharp corners
font-size: 24px; font-size: 24px;
font-weight: 600;
backdrop-filter: blur(10px); 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); } // Tech border effect
&.blue { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); } &::before {
&.purple { background: rgba(168, 85, 247, 0.15); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.3); } 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 { .stats-card {
margin: 0 30px 40px; margin: 40px 40px 60px;
border-radius: 24px; padding: 30px !important;
padding: 30px; background: rgba(20, 20, 20, 0.6) !important;
display: flex; border: 1px solid rgba(255, 255, 255, 0.1) !important;
align-items: center; border-radius: 12px;
justify-content: space-around; position: relative;
backdrop-filter: blur(10px) !important;
animation: fadeInUp 0.8s ease-out 0.2s backwards;
.stat-item { // Corner accents
text-align: center; &::before {
.stat-label { font-size: 24px; color: #888; display: block; margin-bottom: 10px; } content: '';
.stat-value { font-size: 36px; font-weight: bold; color: #fff; } position: absolute;
.price { color: #00b96b; text-shadow: 0 0 10px rgba(0, 185, 107, 0.3); } top: -1px; left: -1px;
.low-stock { color: #ff4d4f; } 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;
} }
.divider { width: 1px; height: 60px; background: rgba(255,255,255,0.1); } .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; }
}
}
} }
// Features Section
.features-section { .features-section {
padding: 0 30px; padding: 0 40px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 40px;
margin-bottom: 40px; margin-bottom: 60px;
.feature-card { .feature-card {
padding: 30px;
border-radius: 20px;
display: flex; 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 { .feature-icon-box {
width: 80px; width: 100px;
height: 80px; height: 100px;
margin-right: 25px; margin-right: 30px;
margin-bottom: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(255,255,255,0.05); background: rgba(0, 0, 0, 0.3);
border-radius: 16px; border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.1);
.f-icon { font-size: 40px; color: #00f0ff; } .f-icon { font-size: 50px; color: #00b96b; }
.f-icon-img { width: 50px; height: 50px; } .f-icon-img { width: 60px; height: 60px; object-fit: contain; }
} }
.feature-text { .feature-text {
flex: 1; flex: 1;
.f-title { font-size: 30px; font-weight: bold; color: #fff; margin-bottom: 10px; display: block; } .f-title { font-size: 32px; font-weight: bold; color: #fff; margin-bottom: 10px; display: block; }
.f-desc { font-size: 24px; color: #aaa; line-height: 1.5; } .f-desc { font-size: 24px; color: #888; line-height: 1.5; }
} }
} }
} }
@@ -158,66 +272,94 @@
.detail-image-section { .detail-image-section {
width: 100%; width: 100%;
margin-bottom: 40px; 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 { .bottom-bar {
position: fixed; position: fixed;
bottom: 0; bottom: 40px;
left: 0; left: 30px;
right: 0; right: 30px;
padding: 20px 30px; height: 110px;
z-index: 100; z-index: 100;
border-top-left-radius: 30px; border-radius: 55px; // Fully rounded capsule
border-top-right-radius: 30px; background: rgba(20, 20, 20, 0.85);
background: rgba(20, 20, 20, 0.95); // Darker for contrast 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 { .action-row {
width: 100%;
height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20px;
height: 100px;
.cart-icon-btn { .btn-add-cart {
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 {
flex: 1; flex: 1;
height: 80px; height: 100%;
line-height: 80px; border-radius: 45px 0 0 45px;
border-radius: 40px; font-size: 30px;
font-size: 28px;
font-weight: bold; font-weight: bold;
border: none; border: none;
margin: 0; margin: 0;
&::after { border: none; }
}
.btn-add-cart {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
color: #fff; color: #fff;
display: flex;
align-items: center;
justify-content: center;
&:active { background: rgba(255, 255, 255, 0.2); }
} }
.btn-buy-now { .btn-buy-now {
background: linear-gradient(90deg, #00b96b, #00f0ff); flex: 1;
color: #000; height: 100%;
box-shadow: 0 5px 20px rgba(0, 185, 107, 0.3); 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 { .safe-area-bottom {
padding-bottom: calc(20px + constant(safe-area-inset-bottom)); padding-bottom: 0;
padding-bottom: calc(20px + env(safe-area-inset-bottom));
} }

View File

@@ -2,6 +2,8 @@ import { View, Text, Image, ScrollView, Button } from '@tarojs/components'
import Taro, { useRouter, useLoad } from '@tarojs/taro' import Taro, { useRouter, useLoad } from '@tarojs/taro'
import { useState } from 'react' import { useState } from 'react'
import { getConfigDetail } from '../../api' import { getConfigDetail } from '../../api'
import ParticleBackground from '../../components/ParticleBackground'
import { addToCart } from '../../utils/cart'
import './detail.scss' import './detail.scss'
export default function Detail() { export default function Detail() {
@@ -26,6 +28,11 @@ export default function Detail() {
} }
} }
const handleAddToCart = () => {
if (!product) return
addToCart(product)
}
const buyNow = () => { const buyNow = () => {
if (!product) return if (!product) return
Taro.navigateTo({ Taro.navigateTo({
@@ -38,12 +45,13 @@ export default function Detail() {
return ( return (
<View className='page-container'> <View className='page-container'>
<ParticleBackground />
<ScrollView scrollY className='content'> <ScrollView scrollY className='content'>
{/* Hero Section */} {/* Hero Section */}
<View className='hero-section'> <View className='hero-section'>
<View className='image-container'> <View className='image-container'>
{product.detail_image_url || product.static_image_url ? ( {product.static_image_url ? (
<Image src={product.detail_image_url || product.static_image_url} mode='widthFix' className='hero-img' /> <Image src={product.static_image_url} mode='widthFix' className='hero-img' />
) : ( ) : (
<View className='placeholder-box'> <View className='placeholder-box'>
<Text className='icon-bolt'></Text> <Text className='icon-bolt'></Text>
@@ -65,44 +73,84 @@ export default function Detail() {
</View> </View>
{/* Stats Section */} {/* Stats Section */}
<View className='stats-card glass-panel'> <View className='stats-card'>
<View className='stat-item'> <View className='label-row'>
<Text className='stat-label'></Text> <Text className='label'></Text>
<Text className='stat-value price'>¥{product.price}</Text> <Text className='label' style={{textAlign: 'right'}}></Text>
</View> </View>
<View className='divider' /> <View className='value-row'>
<View className='stat-item'> <View className='price-box'>
<Text className='stat-label'></Text> <Text className='symbol'>¥</Text>
<Text className={`stat-value ${product.stock < 10 ? 'low-stock' : ''}`}>{product.stock}</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>
</View> </View>
{/* Features Section */} {/* Features Section */}
<View className='features-section'> <View className='features-section'>
{product.features && product.features.length > 0 ? ( {product.features && product.features.length > 0 ? (
product.features.map((f, idx) => ( product.features.map((f, idx) => {
<View key={idx} className='feature-card glass-panel'> let iconContent
<View className='feature-icon-box'> if (f.display_icon) {
{f.icon_url ? <Image src={f.icon_url} className='f-icon-img' /> : <Text className='f-icon'></Text>} 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>
<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> <View className='feature-card'>
<Text className='f-desc'>{product.chip_type} </Text> <View className='feature-icon-box'>
</View> <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> </View>
{/* Detail Image */} {/* Detail Image */}
{product.detail_image_url && ( {(product.display_detail_image || product.detail_image_url) && (
<View className='detail-image-section'> <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> </View>
)} )}
@@ -110,14 +158,15 @@ export default function Detail() {
</ScrollView> </ScrollView>
{/* Bottom Bar */} {/* Bottom Bar */}
<View className='bottom-bar glass-panel safe-area-bottom'> <View className='bottom-bar'>
<View className='action-row'> <View className='action-row'>
<View className='cart-icon-btn' onClick={() => Taro.switchTab({ url: '/pages/cart/cart' })}> <Button className='btn-add-cart' onClick={handleAddToCart}>
<Text className='icon'>🛒</Text> <Text></Text>
<Text className='label'></Text> </Button>
</View> <Button className='btn-buy-now' onClick={buyNow}>
<Button className='btn-add-cart' onClick={() => Taro.showToast({title: '加入购物车', icon:'none'})}></Button> <Text className='cart-icon'>🛒</Text>
<Button className='btn-buy-now' onClick={buyNow}></Button> <Text></Text>
</Button>
</View> </View>
</View> </View>
</View> </View>

View File

@@ -1,52 +1,79 @@
.page-container { .page-container {
height: 100vh; height: 100vh;
background-color: #000; background-color: var(--bg-dark);
color: #fff; color: var(--text-main);
overflow: hidden; overflow: hidden;
position: relative; 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 { .content-scroll {
height: 100vh; height: 100vh;
position: relative; position: relative;
z-index: 1; z-index: 1;
// Ensure no padding here
} }
.scroll-inner { .scroll-inner {
// Container for scroll content
width: 100%; width: 100%;
} }
.header { .header {
text-align: center; text-align: center;
padding: 60px 20px 40px; padding: 80px 24px 60px; // 增加头部留白
position: relative; position: relative;
.logo-box { .logo-box {
margin-bottom: 30px; margin-bottom: 40px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
.logo-img { .logo-img {
width: 120px; width: 140px;
height: 120px; height: 140px;
margin-bottom: 15px; margin-bottom: 20px;
filter: drop-shadow(0 0 15px rgba(0, 240, 255, 0.4)); filter: drop-shadow(0 0 25px rgba(0, 240, 255, 0.5));
animation: float 6s ease-in-out infinite;
} }
.logo-text { .logo-text {
font-size: 40px; font-size: 48px;
font-weight: 900; font-weight: 900;
color: #fff; color: #fff;
letter-spacing: 6px; letter-spacing: 8px;
text-shadow: 0 0 20px rgba(0, 240, 255, 0.6); text-shadow: 0 0 30px rgba(0, 240, 255, 0.7);
} }
} }
.title-container { .title-container {
margin-bottom: 25px; margin-bottom: 30px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -54,26 +81,27 @@
} }
.title-text { .title-text {
font-size: 36px; font-size: 40px;
font-weight: bold; font-weight: 800;
color: #00f0ff; color: var(--primary-cyan);
text-shadow: 0 0 15px rgba(0, 240, 255, 0.5); text-shadow: 0 0 20px rgba(0, 240, 255, 0.4);
} }
.cursor { .cursor {
font-size: 36px; font-size: 40px;
color: #fff; color: #fff;
margin-left: 8px; margin-left: 8px;
animation: blink 1s infinite; animation: blink 1s infinite;
} }
.subtitle { .subtitle {
color: #aaa; color: var(--text-secondary);
font-size: 26px; font-size: 28px;
line-height: 1.6; line-height: 1.8; // 增加行高
display: block; display: block;
padding: 0 40px; padding: 0 40px;
font-weight: 300; font-weight: 400;
letter-spacing: 1px;
} }
} }
@@ -82,40 +110,54 @@
50% { opacity: 0; } 50% { opacity: 0; }
} }
.status-box { @keyframes float {
padding: 100px 0; 0%, 100% { transform: translateY(0); }
text-align: center; 50% { transform: translateY(-10px); }
.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;}
} }
.product-grid { .product-grid {
padding: 0 30px; padding: 0 32px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 40px; gap: 48px; // 增加卡片间距
} }
// 玻璃态卡片升级版
.card { .card {
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(20px); backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(24px);
border-radius: 24px; border-radius: 32px;
overflow: hidden; overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); box-shadow:
transition: all 0.3s ease; 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 { &:active {
transform: scale(0.98); transform: scale(0.96);
border-color: #00b96b; box-shadow:
box-shadow: 0 0 30px rgba(0, 185, 107, 0.2); 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 { &-cover {
height: 360px; height: 400px; // 加大图片区域
background: #111; background: #111;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@@ -123,7 +165,8 @@
.card-img { .card-img {
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: transform 0.5s ease; object-fit: cover;
transition: transform 0.6s ease;
} }
.placeholder-img { .placeholder-img {
@@ -132,8 +175,40 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: radial-gradient(circle at center, #222, #111); background: radial-gradient(circle at center, #1a1a1a, #050505);
.icon-rocket { font-size: 100px; }
.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 { .card-overlay {
@@ -141,44 +216,44 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 50%; height: 60%;
background: linear-gradient(to top, rgba(0,0,0,0.8), transparent); background: linear-gradient(to top, rgba(0,0,0,0.9), transparent);
} }
} }
&-body { &-body {
padding: 30px; padding: 40px 32px;
} }
&-header { &-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
margin-bottom: 15px; margin-bottom: 20px;
.card-title { .card-title {
font-size: 36px; font-size: 40px; // 加大标题
font-weight: bold; font-weight: 700;
color: #fff; color: #fff;
flex: 1; flex: 1;
margin-right: 20px; margin-right: 20px;
line-height: 1.3; line-height: 1.2;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5); text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
} }
.price { .price {
font-size: 36px; font-size: 36px;
color: #00b96b; color: var(--primary-cyan); // 统一用青色或根据产品类型变化
font-weight: 900; font-weight: 800;
text-shadow: 0 0 15px rgba(0, 185, 107, 0.3); text-shadow: 0 0 20px rgba(0, 240, 255, 0.3);
} }
} }
&-desc { &-desc {
font-size: 26px; font-size: 26px;
color: #ccc; color: var(--text-secondary);
line-height: 1.5; line-height: 1.6;
margin-bottom: 25px; margin-bottom: 32px;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
@@ -188,52 +263,126 @@
.tags { .tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; gap: 16px;
margin-bottom: 30px; margin-bottom: 40px;
.tag { .tag {
padding: 8px 18px; padding: 10px 24px;
border-radius: 12px; border-radius: 16px;
font-size: 22px; font-size: 22px;
font-weight: 500; font-weight: 500;
letter-spacing: 0.5px;
&.cyan { &.cyan {
color: #00f0ff; color: var(--primary-cyan);
background: rgba(0, 240, 255, 0.1); background: rgba(0, 240, 255, 0.08);
border: 1px solid rgba(0, 240, 255, 0.3); border: 1px solid rgba(0, 240, 255, 0.2);
} }
&.blue { &.blue {
color: #3b82f6; color: #3b82f6;
background: rgba(59, 130, 246, 0.1); background: rgba(59, 130, 246, 0.08);
border: 1px solid rgba(59, 130, 246, 0.3); border: 1px solid rgba(59, 130, 246, 0.2);
} }
&.purple { &.purple {
color: #a855f7; color: var(--primary-purple);
background: rgba(168, 85, 247, 0.1); background: rgba(189, 0, 255, 0.08);
border: 1px solid rgba(168, 85, 247, 0.3); border: 1px solid rgba(189, 0, 255, 0.2);
} }
} }
} }
&-footer { &-footer {
.btn-buy { .btn-buy {
background: linear-gradient(90deg, #00b96b, #00f0ff); background: linear-gradient(90deg, var(--primary-green), var(--primary-cyan));
color: #000; color: #000;
font-weight: bold; font-weight: 800;
font-size: 30px; font-size: 30px;
border-radius: 50px; border-radius: 60px; // 更圆润
border: none; border: none;
height: 80px; height: 90px;
line-height: 80px; line-height: 90px;
box-shadow: 0 5px 20px rgba(0, 185, 107, 0.3); 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 { &:active {
opacity: 0.9; transform: scale(0.98);
box-shadow: 0 5px 15px rgba(0, 185, 107, 0.2);
} }
} }
} }
} }
.footer-spacer { @keyframes shimmer {
height: 100px; 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);
}
} }

View File

@@ -65,8 +65,10 @@ export default function Index() {
</View> </View>
{loading ? ( {loading ? (
<View className='status-box'> <View className='skeleton-wrapper'>
<Text className='loading-text'>...</Text> {[1, 2, 3].map(i => (
<View key={i} className='skeleton-card' />
))}
</View> </View>
) : error ? ( ) : error ? (
<View className='status-box'> <View className='status-box'>
@@ -75,14 +77,19 @@ export default function Index() {
</View> </View>
) : ( ) : (
<View className='product-grid'> <View className='product-grid'>
{products.map((item) => ( {products.map((item, index) => (
<View key={item.id} className='card' onClick={() => goToDetail(item.id)}> <View
key={item.id}
className='card fade-in-up'
style={{ animationDelay: `${index * 0.1}s` }}
onClick={() => goToDetail(item.id)}
>
<View className='card-cover'> <View className='card-cover'>
{item.static_image_url ? ( {item.static_image_url ? (
<Image src={item.static_image_url} mode='aspectFill' className='card-img' /> <Image src={item.static_image_url} mode='aspectFill' className='card-img' />
) : ( ) : (
<View className='placeholder-img'> <View className='placeholder-img'>
<Text className='icon-rocket'>🚀</Text> <View className='radar-scan'></View>
</View> </View>
)} )}
<View className='card-overlay' /> <View className='card-overlay' />

View File

@@ -1,51 +1,154 @@
.page-container { .page-container {
min-height: 100vh; min-height: 100vh;
background-color: #f7f8fa; background-color: #050505;
padding: 15px; color: #fff;
padding-bottom: 80px; padding-bottom: 120px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
} }
.section { .section {
background: #fff; margin: 20px;
border-radius: 12px; background: rgba(255, 255, 255, 0.03);
padding: 20px; border: 1px solid rgba(255, 255, 255, 0.05);
margin-bottom: 15px; border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.02); 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 { .address-section {
min-height: 80px;
display: flex; display: flex;
flex-direction: column; align-items: center;
justify-content: center; justify-content: space-between;
.row { .address-info {
margin-bottom: 8px; flex: 1;
.name { font-size: 16px; font-weight: bold; margin-right: 10px; } .user-info {
.phone { font-size: 14px; color: #666; } 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;
}
} }
.addr { font-size: 14px; color: #333; line-height: 1.4; }
.placeholder-container { .arrow {
display: flex; font-size: 30px;
justify-content: center; color: #666;
align-items: center; margin-left: 20px;
height: 100%;
} }
.placeholder { font-size: 16px; color: #00b96b; }
} }
.product-section { .product-section {
.p-name { font-size: 16px; font-weight: 500; margin-bottom: 10px; display: block; } padding: 0; // Remove padding for list
.row { display: flex; justify-content: space-between; align-items: center; } overflow: hidden;
.p-price { font-size: 16px; color: #333; }
.p-qty { font-size: 14px; color: #999; }
.divider { height: 1px; background: #eee; margin: 15px 0; } .section-title { margin: 24px 24px 10px; }
.total-row { .product-item {
.total-price { font-size: 20px; color: #ff4d4f; font-weight: bold; } 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 { .bottom-bar {
@@ -53,22 +156,36 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background: #fff; height: 110px;
padding: 10px 20px; background: rgba(20, 20, 20, 0.95);
border-top: 1px solid #eee; 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 { .btn-submit {
background: #00b96b; background: linear-gradient(135deg, #00b96b 0%, #00f0ff 100%);
color: #fff; color: #000;
border-radius: 22px; border-radius: 40px;
padding: 0 60px;
height: 80px;
line-height: 80px;
font-size: 32px;
font-weight: bold;
border: none; border: none;
font-size: 16px; box-shadow: 0 0 20px rgba(0, 185, 107, 0.3);
height: 44px;
line-height: 44px; &: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);
}

View File

@@ -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 Taro, { useRouter, useLoad } from '@tarojs/taro'
import { useState } from 'react' import { useState, useMemo } from 'react'
import { getConfigDetail, createOrder } from '../../api' import { getConfigDetail, createOrder } from '../../api'
import { getSelectedItems, removeItem } from '../../utils/cart'
import './checkout.scss' import './checkout.scss'
export default function Checkout() { export default function Checkout() {
const router = useRouter() const router = useRouter()
const { id, quantity } = router.params const params = router.params
const [product, setProduct] = useState<any>(null) const [items, setItems] = useState<any[]>([])
const [address, setAddress] = useState<any>(null) 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 () => { useLoad(async () => {
if (id) { if (params.from === 'cart') {
const res = await getConfigDetail(Number(id)) const cartItems = getSelectedItems()
setProduct(res) 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 () => { const chooseAddress = async () => {
if (deliveryType === 'pickup') return
try { try {
const res = await Taro.chooseAddress() const res = await Taro.chooseAddress()
setAddress(res) setAddress(res)
setContact({ name: res.userName, phone: res.telNumber }) setUserAddress(res)
} catch (e) { } catch (err) {
Taro.showToast({ title: '需要授权获取地址', icon: 'none' }) 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 () => { const submitOrder = async () => {
if (!address) { if (!address) {
Taro.showToast({ title: '请选择收货地址', icon: 'none' }) Taro.showToast({ title: '请选择收货地址', icon: 'none' })
return return
} }
Taro.showLoading({ title: '提交中...' })
try { try {
Taro.showLoading({ title: '正在下单...' }) const orderPromises = items.map(item => {
const orderData = { const orderData = {
goodid: product.id, goodid: item.id,
quantity: Number(quantity || 1), quantity: item.quantity,
customer_name: contact.name, customer_name: address.userName,
phone_number: contact.phone, phone_number: address.telNumber,
shipping_address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}`, shipping_address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}`
// ref_code: Taro.getStorageSync('ref_code')
} }
return createOrder(orderData)
})
const res = await createOrder(orderData) const results = await Promise.all(orderPromises)
Taro.hideLoading()
// 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' })
}
})
}
if (res.order_id) {
Taro.redirectTo({ url: `/pages/order/payment?id=${res.order_id}` })
}
} catch (err) { } catch (err) {
Taro.hideLoading() Taro.hideLoading()
console.error(err) 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 ( 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}> <View className='section address-section' onClick={chooseAddress}>
{address ? ( {address ? (
<View> <View className='address-info'>
<View className='row'> <View className='user-info'>
<Text className='name'>{contact.name}</Text> <Text>{address.userName}</Text>
<Text className='phone'>{contact.phone}</Text> <Text className='phone'>{address.telNumber}</Text>
</View>
<View className='address-text'>
{address.provinceName}{address.cityName}{address.countyName}{address.detailInfo}
</View> </View>
<Text className='addr'>{address.provinceName}{address.cityName}{address.countyName}{address.detailInfo}</Text>
</View> </View>
) : ( ) : (
<View className='placeholder-container'> <View className='address-info'>
<Text className='placeholder'>+ </Text> <Text className='placeholder'>+ </Text>
</View> </View>
)} )}
{deliveryType === 'delivery' && <Text className='arrow'></Text>}
</View> </View>
{/* Products Section */}
<View className='section product-section'> <View className='section product-section'>
<Text className='p-name'>{product.name}</Text> <Text className='section-title'></Text>
<View className='row'> {items.map((item, idx) => (
<Text className='p-price'>¥{product.price}</Text> <View key={idx} className='product-item'>
<Text className='p-qty'>x {quantity}</Text> <Image src={item.image} className='p-img' mode='aspectFill' />
</View> <View className='p-info'>
<View className='divider' /> <Text className='p-name'>{item.name}</Text>
<View className='row total-row'> <Text className='p-desc'>{item.description}</Text>
<Text></Text> <View className='p-meta'>
<Text className='total-price'>¥{(product.price * (Number(quantity) || 1)).toFixed(2)}</Text> <Text className='p-price'>¥{item.price}</Text>
</View> <Text className='p-qty'>x{item.quantity}</Text>
</View>
</View>
</View>
))}
</View> </View>
<View className='bottom-bar safe-area-bottom'> {/* Summary Section */}
<Button className='btn-submit' onClick={submitOrder}></Button> <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>
</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>
) )
} }

View File

@@ -128,59 +128,158 @@
.process-section { .process-section {
margin-top: 60px; margin-top: 60px;
padding: 40px 20px; padding: 40px 20px;
background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,185,107,0.05) 100%); position: relative;
border-radius: 30px; overflow: hidden;
border: 1px solid rgba(255,255,255,0.05);
// 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 { .section-title {
color: #fff; color: #fff;
text-align: center; text-align: center;
font-size: 36px; font-size: 36px;
font-weight: bold; font-weight: bold;
margin-bottom: 40px; margin-bottom: 60px;
display: block; 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 { .process-steps {
display: flex; display: flex;
flex-wrap: wrap; flex-direction: column;
justify-content: space-between; position: relative;
gap: 20px; 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 { .step-item {
width: 48%; // 2 columns
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
margin-bottom: 30px; margin-bottom: 40px;
position: relative;
&:last-child { margin-bottom: 0; }
.step-icon { .step-icon {
width: 80px; width: 80px;
height: 80px; height: 80px;
border-radius: 24px; border-radius: 20px;
background: rgba(255, 255, 255, 0.03); background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(0, 185, 107, 0.3); border: 1px solid rgba(0, 185, 107, 0.5);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 15px;
color: #00b96b; color: #00b96b;
font-size: 32px; font-size: 36px;
font-weight: bold; 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 { // Content Card
color: #fff; .step-content-wrapper {
font-size: 28px; flex: 1;
font-weight: bold; background: linear-gradient(90deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%);
margin-bottom: 5px; 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;
.step-desc { &:active {
color: #666; background: rgba(255, 255, 255, 0.05);
font-size: 24px; 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; }
}

View File

@@ -91,8 +91,10 @@ export default function ServicesIndex() {
].map((step) => ( ].map((step) => (
<View key={step.id} className='step-item'> <View key={step.id} className='step-item'>
<View className='step-icon'><Text>{step.id}</Text></View> <View className='step-icon'><Text>{step.id}</Text></View>
<Text className='step-title'>{step.title}</Text> <View className='step-content-wrapper'>
<Text className='step-desc'>{step.desc}</Text> <Text className='step-title'>{step.title}</Text>
<Text className='step-desc'>{step.desc}</Text>
</View>
</View> </View>
))} ))}
</View> </View>

View File

@@ -1,53 +1,207 @@
.page-container { .page-container {
min-height: 100vh; 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 { @keyframes pulse {
background: #fff; 0% { box-shadow: 0 0 0 0 rgba(0, 185, 107, 0.4); }
padding: 40px 20px; 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; display: flex;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 30px;
position: relative;
overflow: hidden;
.avatar { .card-bg-effect {
width: 60px; position: absolute;
height: 60px; top: -50%;
border-radius: 30px; right: -20%;
margin-right: 15px; width: 200px;
background: #eee; height: 200px;
background: radial-gradient(circle, rgba(0, 185, 107, 0.2) 0%, transparent 70%);
filter: blur(40px);
z-index: 0;
} }
.nickname { .avatar-container {
font-size: 18px; position: relative;
font-weight: bold; margin-right: 30px;
color: #333; 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 { .stats-row {
background: #fff;
.item {
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; margin-bottom: 30px;
font-size: 16px; padding: 0 10px;
position: relative;
&:last-child { border-bottom: none; } .stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.arrow { color: #ccc; } .stat-val { font-size: 36px; font-weight: bold; color: #fff; margin-bottom: 5px; }
.stat-lbl { font-size: 24px; color: #666; }
.btn-contact { }
position: absolute; }
top: 0;
left: 0; .service-container {
width: 100%; padding-bottom: 40px;
height: 100%;
opacity: 0; .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;
} }
}
} }

View File

@@ -13,33 +13,99 @@ export default function UserIndex() {
const goOrders = () => Taro.navigateTo({ url: '/pages/order/list' }) const goOrders = () => Taro.navigateTo({ url: '/pages/order/list' })
const goDistributor = () => Taro.navigateTo({ url: '/subpackages/distributor/index' }) 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 = () => { const login = () => {
// Trigger login again if needed
Taro.reLaunch({ url: '/pages/index/index' }) 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 ( return (
<View className='page-container'> <View className='page-container'>
<View className='header'> {/* Profile Card */}
<Image src={userInfo?.avatar_url || 'https://via.placeholder.com/100'} className='avatar' /> <View className='profile-card'>
<Text className='nickname'>{userInfo?.nickname || '未登录'}</Text> <View className='avatar-container'>
{!userInfo && <Button size='mini' onClick={login}></Button>} <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>
<View className='menu'> {/* Stats Row */}
<View className='item' onClick={goOrders}> <View className='stats-row'>
<Text></Text> {stats.map((item, idx) => (
<Text className='arrow'>></Text> <View key={idx} className='stat-item'>
</View> <Text className='stat-val'>{item.value}</Text>
<View className='item' onClick={goDistributor}> <Text className='stat-lbl'>{item.label}</Text>
<Text></Text> </View>
<Text className='arrow'>></Text> ))}
</View> </View>
<View className='item'>
<Text></Text> {/* Service Groups */}
<Button openType='contact' className='btn-contact' /> <View className='service-container'>
<Text className='arrow'>></Text> {serviceGroups.map((group, gIdx) => (
</View> <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>
</View> </View>
) )

View File

@@ -30,6 +30,7 @@ export default function DistributorIndex() {
const goInvite = () => Taro.navigateTo({ url: '/subpackages/distributor/invite' }) const goInvite = () => Taro.navigateTo({ url: '/subpackages/distributor/invite' })
const goWithdraw = () => Taro.navigateTo({ url: '/subpackages/distributor/withdraw' }) const goWithdraw = () => Taro.navigateTo({ url: '/subpackages/distributor/withdraw' })
const showComingSoon = () => Taro.showToast({ title: '功能开发中', icon: 'none' })
if (loading) return <View>Loading...</View> if (loading) return <View>Loading...</View>
if (!info) return <View>Error</View> if (!info) return <View>Error</View>
@@ -62,11 +63,11 @@ export default function DistributorIndex() {
<Text>广</Text> <Text>广</Text>
<Text className='arrow'>></Text> <Text className='arrow'>></Text>
</View> </View>
<View className='menu-item'> <View className='menu-item' onClick={showComingSoon}>
<Text></Text> <Text></Text>
<Text className='arrow'>></Text> <Text className='arrow'>></Text>
</View> </View>
<View className='menu-item'> <View className='menu-item' onClick={showComingSoon}>
<Text></Text> <Text></Text>
<Text className='arrow'>></Text> <Text className='arrow'>></Text>
</View> </View>

View 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)
}