diff --git a/backend/check_urls.py b/backend/check_urls.py index 63e303d..6dd3a96 100644 --- a/backend/check_urls.py +++ b/backend/check_urls.py @@ -12,7 +12,7 @@ links = [ "admin:shop_distributor_changelist", "admin:shop_esp32config_changelist", "admin:shop_service_changelist", - "admin:shop_arservice_changelist", + "admin:shop_vbcourse_changelist", "admin:shop_order_changelist", "admin:shop_serviceorder_changelist", "admin:shop_withdrawal_changelist", diff --git a/backend/config/__pycache__/settings.cpython-312.pyc b/backend/config/__pycache__/settings.cpython-312.pyc index 67a96a2..c8aed22 100644 Binary files a/backend/config/__pycache__/settings.cpython-312.pyc and b/backend/config/__pycache__/settings.cpython-312.pyc differ diff --git a/backend/config/settings.py b/backend/config/settings.py index b2702bb..215dbd0 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -232,9 +232,9 @@ UNFOLD = { "link": reverse_lazy("admin:shop_service_changelist"), }, { - "title": "AR体验", - "icon": "view_in_ar", - "link": reverse_lazy("admin:shop_arservice_changelist"), + "title": "VB课程", + "icon": "school", + "link": reverse_lazy("admin:shop_vbcourse_changelist"), }, ], }, diff --git a/backend/db.sqlite3 b/backend/db.sqlite3 index f6de3aa..0cf1b4e 100644 Binary files a/backend/db.sqlite3 and b/backend/db.sqlite3 differ diff --git a/backend/shop/__pycache__/admin.cpython-312.pyc b/backend/shop/__pycache__/admin.cpython-312.pyc index d98e11c..db1d0fe 100644 Binary files a/backend/shop/__pycache__/admin.cpython-312.pyc and b/backend/shop/__pycache__/admin.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/models.cpython-312.pyc b/backend/shop/__pycache__/models.cpython-312.pyc index fa9a345..1961d41 100644 Binary files a/backend/shop/__pycache__/models.cpython-312.pyc and b/backend/shop/__pycache__/models.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/serializers.cpython-312.pyc b/backend/shop/__pycache__/serializers.cpython-312.pyc index ae896a2..8cf1e44 100644 Binary files a/backend/shop/__pycache__/serializers.cpython-312.pyc and b/backend/shop/__pycache__/serializers.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/urls.cpython-312.pyc b/backend/shop/__pycache__/urls.cpython-312.pyc index 2d8534f..f690115 100644 Binary files a/backend/shop/__pycache__/urls.cpython-312.pyc and b/backend/shop/__pycache__/urls.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/views.cpython-312.pyc b/backend/shop/__pycache__/views.cpython-312.pyc index 0d50a17..db1c7c7 100644 Binary files a/backend/shop/__pycache__/views.cpython-312.pyc and b/backend/shop/__pycache__/views.cpython-312.pyc differ diff --git a/backend/shop/admin.py b/backend/shop/admin.py index fc08102..81f2527 100644 --- a/backend/shop/admin.py +++ b/backend/shop/admin.py @@ -4,7 +4,7 @@ from django.db.models import Sum from django import forms from unfold.admin import ModelAdmin, TabularInline from unfold.decorators import display -from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder +from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VBCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder import qrcode from io import BytesIO import base64 @@ -19,11 +19,11 @@ class ExternalUploadWidget(forms.URLInput): super().__init__(*args, **kwargs) self.upload_url = upload_url self.attrs.update({ - 'class': 'upload-url-input', + 'class': 'upload-url-input vTextField', 'data-upload-url': upload_url, 'data-accept': accept, - 'readonly': 'readonly', - 'placeholder': '上传文件后自动生成URL' + 'placeholder': '上传文件后自动生成URL', + 'style': 'width: 100%;' }) class Media: @@ -141,18 +141,26 @@ class ServiceOrderAdmin(ModelAdmin): }), ) -@admin.register(ARService) -class ARServiceAdmin(ModelAdmin): - list_display = ('title', 'created_at') - search_fields = ('title', 'description') +@admin.register(VBCourse) +class VBCourseAdmin(ModelAdmin): + list_display = ('title', 'course_type', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at') + search_fields = ('title', 'description', 'instructor', 'tag') + list_filter = ('course_type', 'instructor', 'tag') fieldsets = ( ('基本信息', { - 'fields': ('title', 'description') + 'fields': ('title', 'description', 'course_type', 'tag') }), - ('封面/长图', { + ('课程详情', { + 'fields': ('instructor', 'duration', 'lesson_count') + }), + ('封面', { 'fields': ('cover_image', 'cover_image_url'), 'description': '图片上传和URL二选一,优先使用URL' }), + ('详情页长图', { + 'fields': ('detail_image', 'detail_image_url'), + 'description': '图片上传和URL二选一,优先使用URL' + }), ) @admin.register(Salesperson) diff --git a/backend/shop/migrations/0018_vbcourse_delete_arservice.py b/backend/shop/migrations/0018_vbcourse_delete_arservice.py new file mode 100644 index 0000000..4975f0f --- /dev/null +++ b/backend/shop/migrations/0018_vbcourse_delete_arservice.py @@ -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', + ), + ] diff --git a/backend/shop/migrations/0019_vbcourse_detail_image_vbcourse_detail_image_url_and_more.py b/backend/shop/migrations/0019_vbcourse_detail_image_vbcourse_detail_image_url_and_more.py new file mode 100644 index 0000000..8a4e192 --- /dev/null +++ b/backend/shop/migrations/0019_vbcourse_detail_image_vbcourse_detail_image_url_and_more.py @@ -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='标签'), + ), + ] diff --git a/backend/shop/migrations/0020_alter_vbcourse_course_type.py b/backend/shop/migrations/0020_alter_vbcourse_course_type.py new file mode 100644 index 0000000..857e060 --- /dev/null +++ b/backend/shop/migrations/0020_alter_vbcourse_course_type.py @@ -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='课程类型'), + ), + ] diff --git a/backend/shop/models.py b/backend/shop/models.py index b1c49b4..2623222 100644 --- a/backend/shop/models.py +++ b/backend/shop/models.py @@ -312,19 +312,36 @@ class ServiceOrder(models.Model): verbose_name_plural = "服务订单列表" -class ARService(models.Model): +class VBCourse(models.Model): """ - AR体验服务模型 + VB Coding 课程模型 """ - title = models.CharField(max_length=100, verbose_name="体验名称") - description = models.TextField(verbose_name="简介") - cover_image = models.ImageField(upload_to='ar/covers/', blank=True, null=True, verbose_name="封面/长图 (上传)") - cover_image_url = models.URLField(blank=True, null=True, verbose_name="封面/长图 (URL)") + COURSE_TYPE_CHOICES = ( + ('software', '软件课程'), + ('hardware', '硬件课程'), + ('incubation', '产品商业孵化'), + ) + + title = models.CharField(max_length=100, verbose_name="课程名称") + description = models.TextField(verbose_name="课程简介") + course_type = models.CharField(max_length=20, choices=COURSE_TYPE_CHOICES, default='software', verbose_name="课程类型") + duration = models.CharField(max_length=50, verbose_name="课程时长", help_text="例如: 30分钟", default="30分钟") + lesson_count = models.IntegerField(default=1, verbose_name="课时数量") + instructor = models.CharField(max_length=50, verbose_name="讲师", default="VB讲师") + + tag = models.CharField(max_length=20, blank=True, verbose_name="标签", help_text="例如: 热门, 推荐, 进阶") + + cover_image = models.ImageField(upload_to='courses/covers/', blank=True, null=True, verbose_name="封面图 (上传)") + cover_image_url = models.URLField(blank=True, null=True, verbose_name="封面图 (URL)") + + detail_image = models.ImageField(upload_to='courses/details/', blank=True, null=True, verbose_name="详情页长图 (上传)") + detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)", help_text="如果填写了URL,将优先使用URL") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") def __str__(self): return self.title class Meta: - verbose_name = "AR体验" - verbose_name_plural = "AR体验管理" + verbose_name = "VB课程" + verbose_name_plural = "VB课程管理" diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py index 6e1fdd1..8c4d7e8 100644 --- a/backend/shop/serializers.py +++ b/backend/shop/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal +from .models import ESP32Config, Order, Salesperson, Service, VBCourse, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal class WeChatUserSerializer(serializers.ModelSerializer): class Meta: @@ -101,14 +101,16 @@ class ServiceOrderSerializer(serializers.ModelSerializer): return super().create(validated_data) -class ARServiceSerializer(serializers.ModelSerializer): +class VBCourseSerializer(serializers.ModelSerializer): """ - AR服务序列化器 + VB课程序列化器 """ display_cover_image = serializers.SerializerMethodField() + display_detail_image = serializers.SerializerMethodField() + course_type_display = serializers.CharField(source='get_course_type_display', read_only=True) class Meta: - model = ARService + model = VBCourse fields = '__all__' def get_display_cover_image(self, obj): @@ -118,6 +120,13 @@ class ARServiceSerializer(serializers.ModelSerializer): return obj.cover_image.url return None + def get_display_detail_image(self, obj): + if obj.detail_image_url: + return obj.detail_image_url + if obj.detail_image: + return obj.detail_image.url + return None + class ESP32ConfigSerializer(serializers.ModelSerializer): """ ESP32配置序列化器 diff --git a/backend/shop/urls.py b/backend/shop/urls.py index 209ab54..3464a6d 100644 --- a/backend/shop/urls.py +++ b/backend/shop/urls.py @@ -2,7 +2,7 @@ from django.urls import path, include, re_path from rest_framework.routers import DefaultRouter from .views import ( ESP32ConfigViewSet, OrderViewSet, order_check_view, - ServiceViewSet, ARServiceViewSet, ServiceOrderViewSet, + ServiceViewSet, VBCourseViewSet, ServiceOrderViewSet, payment_finish, pay, send_sms_code, wechat_login, update_user_info, DistributorViewSet ) @@ -10,7 +10,7 @@ router = DefaultRouter() router.register(r'configs', ESP32ConfigViewSet) router.register(r'orders', OrderViewSet) router.register(r'services', ServiceViewSet) -router.register(r'ar', ARServiceViewSet) +router.register(r'courses', VBCourseViewSet) router.register(r'service-orders', ServiceOrderViewSet) router.register(r'distributor', DistributorViewSet, basename='distributor') diff --git a/backend/shop/views.py b/backend/shop/views.py index 2cb693e..3982f2e 100644 --- a/backend/shop/views.py +++ b/backend/shop/views.py @@ -5,8 +5,8 @@ from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiExample -from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService, ServiceOrder, Salesperson, CommissionLog, WeChatUser, Distributor, Withdrawal -from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer +from .models import ESP32Config, Order, WeChatPayConfig, Service, VBCourse, ServiceOrder, Salesperson, CommissionLog, WeChatUser, Distributor, Withdrawal +from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, VBCourseSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer from django.core.signing import TimestampSigner, BadSignature, SignatureExpired from django.contrib.auth.models import User from wechatpayv3 import WeChatPay, WeChatPayType @@ -508,15 +508,15 @@ def payment_finish(request): return HttpResponse(str(e), status=500) @extend_schema_view( - list=extend_schema(summary="获取AR服务列表", description="获取所有可用的AR服务"), - retrieve=extend_schema(summary="获取AR服务详情", description="获取指定AR服务的详细信息") + list=extend_schema(summary="获取VB课程列表", description="获取所有可用的VB课程"), + retrieve=extend_schema(summary="获取VB课程详情", description="获取指定VB课程的详细信息") ) -class ARServiceViewSet(viewsets.ReadOnlyModelViewSet): +class VBCourseViewSet(viewsets.ReadOnlyModelViewSet): """ - AR服务列表和详情 + VB课程列表和详情 """ - queryset = ARService.objects.all().order_by('-created_at') - serializer_class = ARServiceSerializer + queryset = VBCourse.objects.all().order_by('-created_at') + serializer_class = VBCourseSerializer def order_check_view(request): """ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 50bef69..f44e2cf 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,7 +6,7 @@ import ProductDetail from './pages/ProductDetail'; import Payment from './pages/Payment'; import AIServices from './pages/AIServices'; import ServiceDetail from './pages/ServiceDetail'; -import ARExperience from './pages/ARExperience'; +import VBCourses from './pages/VBCourses'; import MyOrders from './pages/MyOrders'; import 'antd/dist/reset.css'; import './App.css'; @@ -19,7 +19,7 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/api.js b/frontend/src/api.js index 48225bf..f01ac76 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -19,7 +19,7 @@ export const confirmPayment = (orderId) => api.post(`/orders/${orderId}/confirm_ export const getServices = () => api.get('/services/'); export const getServiceDetail = (id) => api.get(`/services/${id}/`); export const createServiceOrder = (data) => api.post('/service-orders/', data); -export const getARServices = () => api.get('/ar/'); +export const getVBCourses = () => api.get('/courses/'); export const sendSms = (data) => api.post('/auth/send-sms/', data); export const queryMyOrders = (data) => api.post('/orders/my_orders/', data); diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index ad6242d..6adaa09 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -34,9 +34,9 @@ const Layout = ({ children }) => { label: 'AI 服务', }, { - key: '/ar', + key: '/courses', icon: , - label: 'AR 体验', + label: 'VB 课程', }, { key: '/my-orders', diff --git a/frontend/src/pages/ARExperience.jsx b/frontend/src/pages/VBCourses.jsx similarity index 57% rename from frontend/src/pages/ARExperience.jsx rename to frontend/src/pages/VBCourses.jsx index 6dcc837..910c81f 100644 --- a/frontend/src/pages/ARExperience.jsx +++ b/frontend/src/pages/VBCourses.jsx @@ -1,28 +1,27 @@ import React, { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; -import { Button, Typography, Spin, Row, Col, Empty } from 'antd'; -import { ScanOutlined } from '@ant-design/icons'; -import { getARServices } from '../api'; +import { Button, Typography, Spin, Row, Col, Empty, Tag } from 'antd'; +import { ReadOutlined, ClockCircleOutlined, UserOutlined, BookOutlined } from '@ant-design/icons'; +import { getVBCourses } from '../api'; const { Title, Paragraph } = Typography; -const ARExperience = () => { - const [scanning, setScanning] = useState(true); - const [arServices, setArServices] = useState([]); +const VBCourses = () => { + const [courses, setCourses] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { - const fetchAR = async () => { + const fetchCourses = async () => { try { - const res = await getARServices(); - setArServices(res.data); + const res = await getVBCourses(); + setCourses(res.data); } catch (error) { - console.error("Failed to fetch AR services:", error); + console.error("Failed to fetch VB Courses:", error); } finally { setLoading(false); } } - fetchAR(); + fetchCourses(); }, []); if (loading) return
; @@ -31,20 +30,20 @@ const ARExperience = () => {
- AR <span style={{ color: '#00f0ff' }}>UNIVERSE</span> + VB <span style={{ color: '#00f0ff' }}>CODING COURSES</span> - 探索全息增强现实体验。请佩戴您的设备,或使用移动端摄像头扫描空间。 + 探索 Vibe Coding 软件与硬件课程,开启您的编程之旅。
- {arServices.length === 0 ? ( + {courses.length === 0 ? (
- 暂无 AR 体验内容} /> + 暂无课程内容} />
) : ( - {arServices.map((item, index) => ( + {courses.map((item, index) => ( { border: '1px solid rgba(0,240,255,0.2)', borderRadius: 12, overflow: 'hidden', - height: '100%' + height: '100%', + display: 'flex', + flexDirection: 'column' }}> -
+
{item.display_cover_image ? ( {item.title} ) : ( - + )} +
+ {item.tag && ( + {item.tag} + )} + + {item.course_type_display || (item.course_type === 'hardware' ? '硬件课程' : '软件课程')} + +
-
-

{item.title}

-

{item.description}

+
+

{item.title}

+
+ {item.instructor} + {item.duration} + {item.lesson_count} 课时 +
+

{item.description}

@@ -108,4 +122,4 @@ const ARExperience = () => { ); }; -export default ARExperience; +export default VBCourses; diff --git a/miniprogram/config/index.js b/miniprogram/config/index.js index 77000c6..2d4ff5e 100644 --- a/miniprogram/config/index.js +++ b/miniprogram/config/index.js @@ -22,7 +22,7 @@ const config = { framework: 'react', compiler: 'webpack5', cache: { - enable: false // Disable cache to avoid potential issues + enable: true // Enable cache for better build performance }, mini: { postcss: { diff --git a/miniprogram/src/api/index.ts b/miniprogram/src/api/index.ts index 433f2d9..6bdc308 100644 --- a/miniprogram/src/api/index.ts +++ b/miniprogram/src/api/index.ts @@ -15,15 +15,18 @@ export const getServices = () => request({ url: '/services/' }) export const getServiceDetail = (id: number) => request({ url: `/services/${id}/` }) export const createServiceOrder = (data: any) => request({ url: '/service-orders/', method: 'POST', data }) -// AR Services -export const getARServices = () => request({ url: '/ar/' }) -export const getARServiceDetail = (id: number) => request({ url: `/ar/${id}/` }) +// VB Courses +export const getVBCourses = () => request({ url: '/courses/' }) +export const getVBCourseDetail = (id: number) => request({ url: `/courses/${id}/` }) // Distributor export const distributorRegister = (data: any) => request({ url: '/distributor/register/', method: 'POST', data }) export const distributorInfo = () => request({ url: '/distributor/info/' }) export const distributorInvite = () => request({ url: '/distributor/invite/', method: 'POST' }) export const distributorWithdraw = (amount: number) => request({ url: '/distributor/withdraw/', method: 'POST', data: { amount } }) +// TODO: Verify if these exist in the API docs +// export const distributorTeam = () => request({ url: '/distributor/team/' }) +// export const distributorHistory = () => request({ url: '/distributor/history/' }) // User export const updateUserInfo = (data: any) => request({ url: '/wechat/update/', method: 'POST', data }) diff --git a/miniprogram/src/app.config.ts b/miniprogram/src/app.config.ts index 41e0fa3..187cb73 100644 --- a/miniprogram/src/app.config.ts +++ b/miniprogram/src/app.config.ts @@ -3,8 +3,8 @@ export default defineAppConfig({ 'pages/index/index', 'pages/services/index', 'pages/services/detail', - 'pages/ar/index', - 'pages/ar/detail', + 'pages/courses/index', + 'pages/courses/detail', 'pages/goods/detail', 'pages/cart/cart', 'pages/order/checkout', @@ -25,14 +25,15 @@ export default defineAppConfig({ ], window: { backgroundTextStyle: 'light', - navigationBarBackgroundColor: '#fff', + navigationBarBackgroundColor: '#000000', navigationBarTitleText: 'Quant Speed Market', - navigationBarTextStyle: 'black' + navigationBarTextStyle: 'white' }, tabBar: { - color: "#999", - selectedColor: "#333", - backgroundColor: "#fff", + color: "#666666", + selectedColor: "#00b96b", + backgroundColor: "#000000", + borderStyle: "black", list: [ { pagePath: "pages/index/index", @@ -43,13 +44,19 @@ export default defineAppConfig({ { pagePath: "pages/services/index", text: "AI服务", - iconPath: "./assets/cart.png", // Using cart icon as placeholder if no other icon available - selectedIconPath: "./assets/cart_active.png" + iconPath: "./assets/AI_service.png", + selectedIconPath: "./assets/AI_service_active.png" }, { - pagePath: "pages/ar/index", - text: "AR体验", - iconPath: "./assets/cart.png", // Placeholder + pagePath: "pages/courses/index", + text: "VB课程", + iconPath: "./assets/VR.png", + selectedIconPath: "./assets/VR_active.png" + }, + { + pagePath: "pages/cart/cart", + text: "购物车", + iconPath: "./assets/cart.png", selectedIconPath: "./assets/cart_active.png" }, { diff --git a/miniprogram/src/app.scss b/miniprogram/src/app.scss index 237a53a..55d4031 100644 --- a/miniprogram/src/app.scss +++ b/miniprogram/src/app.scss @@ -1,8 +1,18 @@ page { - background-color: #f7f8fa; + --primary-cyan: #00f0ff; + --primary-green: #00b96b; + --primary-purple: #bd00ff; + --bg-dark: #050505; + --card-bg: rgba(255, 255, 255, 0.03); + --glass-border: rgba(255, 255, 255, 0.08); + --text-main: #ffffff; + --text-secondary: rgba(255, 255, 255, 0.7); + + background-color: var(--bg-dark); font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif; + color: var(--text-main); } .container { diff --git a/miniprogram/src/assets/AI_service.png b/miniprogram/src/assets/AI_service.png new file mode 100644 index 0000000..b84a007 Binary files /dev/null and b/miniprogram/src/assets/AI_service.png differ diff --git a/miniprogram/src/assets/AI_service_active.png b/miniprogram/src/assets/AI_service_active.png new file mode 100644 index 0000000..e6364e0 Binary files /dev/null and b/miniprogram/src/assets/AI_service_active.png differ diff --git a/miniprogram/src/assets/VR.png b/miniprogram/src/assets/VR.png new file mode 100644 index 0000000..c22d66a Binary files /dev/null and b/miniprogram/src/assets/VR.png differ diff --git a/miniprogram/src/assets/VR_active.png b/miniprogram/src/assets/VR_active.png new file mode 100644 index 0000000..12555b1 Binary files /dev/null and b/miniprogram/src/assets/VR_active.png differ diff --git a/miniprogram/src/assets/cart.png b/miniprogram/src/assets/cart.png index 7484fd0..2f7aff5 100644 Binary files a/miniprogram/src/assets/cart.png and b/miniprogram/src/assets/cart.png differ diff --git a/miniprogram/src/assets/cart_active.png b/miniprogram/src/assets/cart_active.png index 270e748..958df53 100644 Binary files a/miniprogram/src/assets/cart_active.png and b/miniprogram/src/assets/cart_active.png differ diff --git a/miniprogram/src/assets/home.png b/miniprogram/src/assets/home.png index 7484fd0..3961563 100644 Binary files a/miniprogram/src/assets/home.png and b/miniprogram/src/assets/home.png differ diff --git a/miniprogram/src/assets/home_active.png b/miniprogram/src/assets/home_active.png index 270e748..5f3a3e8 100644 Binary files a/miniprogram/src/assets/home_active.png and b/miniprogram/src/assets/home_active.png differ diff --git a/miniprogram/src/assets/logo.svg b/miniprogram/src/assets/logo.svg index e2c1f43..1972a12 100644 --- a/miniprogram/src/assets/logo.svg +++ b/miniprogram/src/assets/logo.svg @@ -1,6 +1,104 @@ - - - - - + + + + +Created by potrace 1.10, written by Peter Selinger 2001-2011 + + + + + + + + + + + + + + + diff --git a/miniprogram/src/assets/user.png b/miniprogram/src/assets/user.png index 7484fd0..f8c3399 100644 Binary files a/miniprogram/src/assets/user.png and b/miniprogram/src/assets/user.png differ diff --git a/miniprogram/src/assets/user_active.png b/miniprogram/src/assets/user_active.png index 270e748..290c909 100644 Binary files a/miniprogram/src/assets/user_active.png and b/miniprogram/src/assets/user_active.png differ diff --git a/miniprogram/src/pages/ar/detail.tsx b/miniprogram/src/pages/ar/detail.tsx deleted file mode 100644 index ffaceb3..0000000 --- a/miniprogram/src/pages/ar/detail.tsx +++ /dev/null @@ -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(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 Loading... - if (!detail) return Not Found - - return ( - - {detail.title} - {detail.description} - - - 📷 - AR 场景加载区域 - - - - - ) -} diff --git a/miniprogram/src/pages/cart/cart.scss b/miniprogram/src/pages/cart/cart.scss index 4de2841..1f4e69f 100644 --- a/miniprogram/src/pages/cart/cart.scss +++ b/miniprogram/src/pages/cart/cart.scss @@ -1,8 +1,214 @@ .page-container { - min-height: 100vh; - background-color: #f7f8fa; - display: flex; - justify-content: center; - align-items: center; + min-height: 100vh; + background-color: #050505; + color: #fff; + padding-bottom: 120px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; +} + +.empty-state { + height: 80vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .empty-icon { + font-size: 80px; + margin-bottom: 20px; + opacity: 0.5; + } + + .empty-text { + font-size: 28px; + color: #666; + } +} + +.cart-list { + padding: 20px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.cart-item { + display: flex; + align-items: center; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 16px; + padding: 20px; + backdrop-filter: blur(10px); + + .checkbox-area { + padding: 10px; + margin-right: 10px; + + .checkbox { + width: 40px; + height: 40px; + border-radius: 50%; + border: 2px solid #666; + display: flex; + align-items: center; + justify-content: center; + + &.checked { + border-color: #00b96b; + background: rgba(0, 185, 107, 0.2); + color: #00b96b; + } + } + } + + .item-img { + width: 160px; + height: 160px; + border-radius: 12px; + margin-right: 20px; + background: #000; + object-fit: cover; + } + + .item-info { + flex: 1; + height: 160px; + display: flex; + flex-direction: column; + justify-content: space-between; + + .item-name { + font-size: 30px; + font-weight: bold; + color: #fff; + margin-bottom: 8px; + } + + .item-desc { + font-size: 24px; + color: #888; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .price-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: auto; + + .price { + font-size: 32px; + color: #00b96b; + font-weight: bold; + } + + .quantity-control { + display: flex; + align-items: center; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + padding: 4px; + + .btn-qty { + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + font-size: 32px; + color: #fff; + + &:active { opacity: 0.7; } + } + + .qty-num { + width: 60px; + text-align: center; + font-size: 28px; + font-weight: bold; + } + } + } + } + + .btn-delete { + padding: 10px; + margin-left: 10px; + color: #ff4d4f; + font-size: 32px; + } +} + +.bottom-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 110px; + background: rgba(20, 20, 20, 0.95); + backdrop-filter: blur(20px); + border-top: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 30px; + z-index: 100; + + .left-section { + display: flex; + align-items: center; + + .select-all-btn { + display: flex; + align-items: center; + margin-right: 30px; + + .checkbox { + width: 36px; + height: 36px; + border-radius: 50%; + border: 2px solid #666; + margin-right: 10px; + display: flex; + align-items: center; + justify-content: center; + + &.checked { + border-color: #00b96b; + background: rgba(0, 185, 107, 0.2); + color: #00b96b; + } + } + + .label { font-size: 28px; color: #fff; } + } + + .total-info { + .label { font-size: 24px; color: #888; margin-right: 10px; } + .price { font-size: 40px; color: #00b96b; font-weight: bold; } + } + } + + .btn-checkout { + background: linear-gradient(135deg, #00b96b 0%, #00f0ff 100%); + color: #000; + border-radius: 40px; + padding: 0 50px; + height: 80px; + line-height: 80px; + font-size: 32px; + font-weight: bold; + border: none; + box-shadow: 0 0 20px rgba(0, 185, 107, 0.3); + + &:active { transform: scale(0.98); } + &.disabled { + background: #333; + color: #666; + box-shadow: none; + } + } } -.empty { color: #999; font-size: 16px; } diff --git a/miniprogram/src/pages/cart/cart.tsx b/miniprogram/src/pages/cart/cart.tsx index c70933b..45433a2 100644 --- a/miniprogram/src/pages/cart/cart.tsx +++ b/miniprogram/src/pages/cart/cart.tsx @@ -1,12 +1,145 @@ -import { View, Text } from '@tarojs/components' +import { View, Text, Image, ScrollView, Button } from '@tarojs/components' +import Taro, { useDidShow } from '@tarojs/taro' +import { useState, useMemo } from 'react' +import { getCart, updateQuantity, removeItem, toggleSelect, toggleSelectAll, CartItem } from '../../utils/cart' import './cart.scss' export default function Cart() { + const [cartItems, setCartItems] = useState([]) + + 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 ( - - 购物车功能即将上线 - + {cartItems.length === 0 ? ( + + 🛒 + 购物车空空如也 + + + ) : ( + + {cartItems.map(item => ( + + handleToggle(item.id)}> + + {item.selected && } + + + + + + + + {item.name} + {/* {item.description} */} + + + + ¥{item.price} + + + handleUpdateQuantity(item.id, -1)}>− + {item.quantity} + handleUpdateQuantity(item.id, 1)}>+ + + + + + handleRemove(item.id)}>× + + ))} + + )} + + {cartItems.length > 0 && ( + + + + + {isAllSelected && } + + 全选 + + + + 合计: + ¥{totalPrice} + + + + + + )} ) } diff --git a/miniprogram/src/pages/ar/detail.config.ts b/miniprogram/src/pages/courses/detail.config.ts similarity index 100% rename from miniprogram/src/pages/ar/detail.config.ts rename to miniprogram/src/pages/courses/detail.config.ts diff --git a/miniprogram/src/pages/ar/detail.scss b/miniprogram/src/pages/courses/detail.scss similarity index 67% rename from miniprogram/src/pages/ar/detail.scss rename to miniprogram/src/pages/courses/detail.scss index 8f35c4c..1adc137 100644 --- a/miniprogram/src/pages/ar/detail.scss +++ b/miniprogram/src/pages/courses/detail.scss @@ -13,6 +13,28 @@ display: block; } +.meta-info { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 30px; + align-items: center; + + .tag { + background: rgba(0, 240, 255, 0.2); + color: #00f0ff; + padding: 6px 16px; + border-radius: 4px; + font-size: 24px; + border: 1px solid #00f0ff; + } + + .info { + color: #888; + font-size: 26px; + } +} + .desc { color: #aaa; font-size: 28px; @@ -20,7 +42,7 @@ display: block; } -.ar-placeholder { +.course-placeholder { width: 100%; height: 500px; background: #111; diff --git a/miniprogram/src/pages/courses/detail.tsx b/miniprogram/src/pages/courses/detail.tsx new file mode 100644 index 0000000..ab29a86 --- /dev/null +++ b/miniprogram/src/pages/courses/detail.tsx @@ -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(null) + const [loading, setLoading] = useState(true) + + useLoad((options) => { + if (options.id) fetchDetail(options.id) + }) + + const typeMap: Record = { + 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 Loading... + if (!detail) return Not Found + + return ( + + {detail.title} + + + {typeMap[detail.course_type] || '软件课程'} + 讲师: {detail.instructor} + 时长: {detail.duration} + 课时: {detail.lesson_count} + + + {detail.description} + + + {detail.display_detail_image ? ( + + ) : ( + <> + 📚 + 课程大纲与视频内容加载区域 + + )} + + + + + ) +} diff --git a/miniprogram/src/pages/ar/index.config.ts b/miniprogram/src/pages/courses/index.config.ts similarity index 100% rename from miniprogram/src/pages/ar/index.config.ts rename to miniprogram/src/pages/courses/index.config.ts diff --git a/miniprogram/src/pages/ar/index.scss b/miniprogram/src/pages/courses/index.scss similarity index 71% rename from miniprogram/src/pages/ar/index.scss rename to miniprogram/src/pages/courses/index.scss index a0a4c70..179338d 100644 --- a/miniprogram/src/pages/ar/index.scss +++ b/miniprogram/src/pages/courses/index.scss @@ -68,6 +68,33 @@ font-size: 80px; font-weight: bold; } + + .tag-container { + position: absolute; + top: 10px; + right: 10px; + display: flex; + gap: 10px; + } + + .type-tag { + background: rgba(0, 240, 255, 0.2); + border: 1px solid #00f0ff; + padding: 4px 12px; + border-radius: 4px; + .type-text { + color: #00f0ff; + font-size: 20px; + } + + &.special { + background: rgba(255, 87, 34, 0.2); + border: 1px solid #ff5722; + .type-text { + color: #ff5722; + } + } + } } .content { @@ -79,6 +106,16 @@ margin-bottom: 15px; display: block; } + + .info-row { + display: flex; + gap: 20px; + margin-bottom: 10px; + .info-text { + color: #aaa; + font-size: 24px; + } + } .item-desc { color: #888; diff --git a/miniprogram/src/pages/ar/index.tsx b/miniprogram/src/pages/courses/index.tsx similarity index 66% rename from miniprogram/src/pages/ar/index.tsx rename to miniprogram/src/pages/courses/index.tsx index 6c5c3dd..b984c0f 100644 --- a/miniprogram/src/pages/ar/index.tsx +++ b/miniprogram/src/pages/courses/index.tsx @@ -1,21 +1,21 @@ import { View, Text, Image, Button } from '@tarojs/components' import Taro, { useLoad } from '@tarojs/taro' import { useState } from 'react' -import { getARServices } from '../../api' +import { getVBCourses } from '../../api' import './index.scss' -export default function ARIndex() { - const [arList, setArList] = useState([]) +export default function CourseIndex() { + const [courseList, setCourseList] = useState([]) const [loading, setLoading] = useState(true) useLoad(() => { - fetchAR() + fetchCourses() }) - const fetchAR = async () => { + const fetchCourses = async () => { try { - const res: any = await getARServices() - setArList(res.results || res) + const res: any = await getVBCourses() + setCourseList(res.results || res) } catch (err) { console.error(err) Taro.showToast({ title: '加载失败', icon: 'none' }) @@ -25,7 +25,7 @@ export default function ARIndex() { } const goDetail = (id: number) => { - Taro.navigateTo({ url: `/pages/ar/detail?id=${id}` }) + Taro.navigateTo({ url: `/pages/courses/detail?id=${id}` }) } if (loading) return Loading... @@ -35,29 +35,29 @@ export default function ARIndex() { - AR UNIVERSE - 探索全息增强现实体验 + VB COURSES + 探索 VB 编程课程 - {arList.length === 0 ? ( + {courseList.length === 0 ? ( - 暂无 AR 体验内容 + 暂无 VB 课程内容 ) : ( - arList.map((item) => ( + courseList.map((item) => ( goDetail(item.id)}> {item.cover_image_url ? ( ) : ( - AR + VB )} {item.title} {item.description} - + )) diff --git a/miniprogram/src/pages/goods/detail.scss b/miniprogram/src/pages/goods/detail.scss index fd9973f..b731a92 100644 --- a/miniprogram/src/pages/goods/detail.scss +++ b/miniprogram/src/pages/goods/detail.scss @@ -1,8 +1,10 @@ .page-container { - height: 100vh; - background-color: #000; + min-height: 100vh; + background-color: #050505; color: #fff; position: relative; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + overflow-x: hidden; } .loading-screen, .error-screen { @@ -10,147 +12,259 @@ display: flex; align-items: center; justify-content: center; - color: #666; + color: #00f0ff; + background: #000; + font-size: 28px; + letter-spacing: 2px; } .content { height: 100vh; - background: #000; + position: relative; + z-index: 1; + padding-bottom: 200px; // Ensure scroll space for bottom bar } -.glass-panel { - background: rgba(255, 255, 255, 0.05); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +// Animations +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(40px); } + to { opacity: 1; transform: translateY(0); } } +@keyframes float { + 0% { transform: translateY(0px); } + 50% { transform: translateY(-20px); } + 100% { transform: translateY(0px); } +} + +@keyframes pulse-glow { + 0% { box-shadow: 0 0 10px rgba(0, 185, 107, 0.4); } + 50% { box-shadow: 0 0 25px rgba(0, 185, 107, 0.8), 0 0 10px rgba(0, 240, 255, 0.4); } + 100% { box-shadow: 0 0 10px rgba(0, 185, 107, 0.4); } +} + +@keyframes scanline { + 0% { top: -10%; opacity: 0; } + 50% { opacity: 1; } + 100% { top: 110%; opacity: 0; } +} + +// Hero Section .hero-section { position: relative; - margin-bottom: 20px; + margin-bottom: 40px; + animation: fadeInUp 0.8s ease-out; .image-container { width: 100%; - min-height: 600px; - background: radial-gradient(circle at center, #1a1a1a, #000); + min-height: 600px; // Slightly reduced to fit better + background: radial-gradient(circle at center, rgba(0, 240, 255, 0.05) 0%, transparent 70%); position: relative; display: flex; align-items: center; justify-content: center; + overflow: hidden; + + // Scanline effect + &::after { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(to right, transparent, rgba(0, 240, 255, 0.5), transparent); + animation: scanline 3s linear infinite; + z-index: 0; + } .hero-img { - width: 100%; + width: 75%; + height: auto; display: block; + filter: drop-shadow(0 0 40px rgba(0, 240, 255, 0.2)); + animation: float 6s ease-in-out infinite; + z-index: 1; } .placeholder-box { - .icon-bolt { font-size: 100px; } - } - - .hero-overlay { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 60%; - background: linear-gradient(to top, #000 10%, transparent); + .icon-bolt { font-size: 150px; color: #00b96b; text-shadow: 0 0 30px rgba(0, 185, 107, 0.5); } } } .hero-content { - padding: 0 30px; - margin-top: -100px; // Pull up over image + padding: 0 40px; + margin-top: -40px; position: relative; z-index: 2; .hero-title { - font-size: 48px; + font-size: 60px; font-weight: 900; color: #fff; display: block; - margin-bottom: 15px; - text-shadow: 0 0 20px rgba(0,0,0,0.8); + margin-bottom: 24px; + line-height: 1.1; + text-shadow: 0 0 20px rgba(0, 240, 255, 0.3); + letter-spacing: 1px; } .hero-desc { font-size: 28px; - color: #ccc; - line-height: 1.5; + color: rgba(255, 255, 255, 0.7); + line-height: 1.6; display: block; - margin-bottom: 25px; - text-shadow: 0 0 10px rgba(0,0,0,0.8); + margin-bottom: 32px; + font-weight: 300; } .tags-row { display: flex; flex-wrap: wrap; - gap: 15px; + gap: 16px; .tag { - padding: 8px 20px; - border-radius: 30px; + padding: 10px 28px; + border-radius: 4px; // Techy sharp corners font-size: 24px; + font-weight: 600; backdrop-filter: blur(10px); + position: relative; + overflow: hidden; - &.cyan { background: rgba(0, 240, 255, 0.15); color: #00f0ff; border: 1px solid rgba(0, 240, 255, 0.3); } - &.blue { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); } - &.purple { background: rgba(168, 85, 247, 0.15); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.3); } + // Tech border effect + &::before { + content: ''; + position: absolute; + top: 0; left: 0; width: 4px; height: 100%; + } + + &.cyan { + color: #00f0ff; + background: rgba(0, 240, 255, 0.08); + border: 1px solid rgba(0, 240, 255, 0.3); + &::before { background: #00f0ff; } + } + &.blue { + color: #3b82f6; + background: rgba(59, 130, 246, 0.08); + border: 1px solid rgba(59, 130, 246, 0.3); + &::before { background: #3b82f6; } + } + &.purple { + color: #a855f7; + background: rgba(168, 85, 247, 0.08); + border: 1px solid rgba(168, 85, 247, 0.3); + &::before { background: #a855f7; } + } } } } } +// Stats Card (HUD Style) .stats-card { - margin: 0 30px 40px; - border-radius: 24px; - padding: 30px; - display: flex; - align-items: center; - justify-content: space-around; - - .stat-item { - text-align: center; - .stat-label { font-size: 24px; color: #888; display: block; margin-bottom: 10px; } - .stat-value { font-size: 36px; font-weight: bold; color: #fff; } - .price { color: #00b96b; text-shadow: 0 0 10px rgba(0, 185, 107, 0.3); } - .low-stock { color: #ff4d4f; } + margin: 40px 40px 60px; + padding: 30px !important; + background: rgba(20, 20, 20, 0.6) !important; + border: 1px solid rgba(255, 255, 255, 0.1) !important; + border-radius: 12px; + position: relative; + backdrop-filter: blur(10px) !important; + animation: fadeInUp 0.8s ease-out 0.2s backwards; + + // Corner accents + &::before { + content: ''; + position: absolute; + top: -1px; left: -1px; + width: 20px; height: 20px; + border-top: 2px solid #00b96b; + border-left: 2px solid #00b96b; + border-top-left-radius: 12px; + } + &::after { + content: ''; + position: absolute; + bottom: -1px; right: -1px; + width: 20px; height: 20px; + border-bottom: 2px solid #00b96b; + border-right: 2px solid #00b96b; + border-bottom-right-radius: 12px; + } + + .label-row { + display: flex; + width: 100%; + margin-bottom: 12px; + .label { font-size: 24px; color: #666; flex: 1; text-transform: uppercase; letter-spacing: 1px; } + } + + .value-row { + display: flex; + width: 100%; + align-items: baseline; + + .price-box { + flex: 1; + display: flex; + align-items: baseline; + .symbol { font-size: 32px; color: #00b96b; font-weight: bold; margin-right: 4px; } + .price { + font-size: 72px; + color: #00b96b; + font-weight: bold; + text-shadow: 0 0 25px rgba(0, 185, 107, 0.4); + font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; // Ensure clean number font + } + } + + .stock-box { + .stock { font-size: 36px; color: #fff; font-weight: bold; } + .unit { font-size: 24px; color: #666; margin-left: 6px; } + } } - - .divider { width: 1px; height: 60px; background: rgba(255,255,255,0.1); } } +// Features Section .features-section { - padding: 0 30px; + padding: 0 40px; display: flex; flex-direction: column; - gap: 20px; - margin-bottom: 40px; + gap: 40px; + margin-bottom: 60px; .feature-card { - padding: 30px; - border-radius: 20px; display: flex; - align-items: flex-start; + flex-direction: row; // Change to row for better list layout + align-items: center; + text-align: left; + background: rgba(255, 255, 255, 0.03) !important; + border: 1px solid rgba(255, 255, 255, 0.05) !important; + border-radius: 16px; + padding: 30px; + animation: fadeInUp 0.8s ease-out; + // Stagger animations manually or via JS (here simplified) .feature-icon-box { - width: 80px; - height: 80px; - margin-right: 25px; + width: 100px; + height: 100px; + margin-right: 30px; + margin-bottom: 0; display: flex; align-items: center; justify-content: center; - background: rgba(255,255,255,0.05); - border-radius: 16px; + background: rgba(0, 0, 0, 0.3); + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.1); - .f-icon { font-size: 40px; color: #00f0ff; } - .f-icon-img { width: 50px; height: 50px; } + .f-icon { font-size: 50px; color: #00b96b; } + .f-icon-img { width: 60px; height: 60px; object-fit: contain; } } .feature-text { flex: 1; - .f-title { font-size: 30px; font-weight: bold; color: #fff; margin-bottom: 10px; display: block; } - .f-desc { font-size: 24px; color: #aaa; line-height: 1.5; } + .f-title { font-size: 32px; font-weight: bold; color: #fff; margin-bottom: 10px; display: block; } + .f-desc { font-size: 24px; color: #888; line-height: 1.5; } } } } @@ -158,66 +272,94 @@ .detail-image-section { width: 100%; margin-bottom: 40px; - .long-detail-img { width: 100%; display: block; } + position: relative; + + // Decorative line top + &::before { + content: ''; + display: block; + width: 100px; + height: 4px; + background: #333; + margin: 0 auto 40px; + border-radius: 2px; + } + + .long-detail-img { width: 100%; height: auto; display: block; } } -.footer-spacer { height: 160px; } +.footer-spacer { height: 200px; } +// Bottom Bar .bottom-bar { position: fixed; - bottom: 0; - left: 0; - right: 0; - padding: 20px 30px; + bottom: 40px; + left: 30px; + right: 30px; + height: 110px; z-index: 100; - border-top-left-radius: 30px; - border-top-right-radius: 30px; - background: rgba(20, 20, 20, 0.95); // Darker for contrast + border-radius: 55px; // Fully rounded capsule + background: rgba(20, 20, 20, 0.85); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 10px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; .action-row { + width: 100%; + height: 100%; display: flex; align-items: center; - gap: 20px; - height: 100px; - .cart-icon-btn { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 0 20px; - - .icon { font-size: 40px; margin-bottom: 5px; } - .label { font-size: 20px; color: #888; } - } - - .btn-add-cart, .btn-buy-now { + .btn-add-cart { flex: 1; - height: 80px; - line-height: 80px; - border-radius: 40px; - font-size: 28px; + height: 100%; + border-radius: 45px 0 0 45px; + font-size: 30px; font-weight: bold; border: none; margin: 0; - - &::after { border: none; } - } - - .btn-add-cart { background: rgba(255, 255, 255, 0.1); color: #fff; + display: flex; + align-items: center; + justify-content: center; + + &:active { background: rgba(255, 255, 255, 0.2); } } - + .btn-buy-now { - background: linear-gradient(90deg, #00b96b, #00f0ff); - color: #000; - box-shadow: 0 5px 20px rgba(0, 185, 107, 0.3); + flex: 1; + height: 100%; + border-radius: 0 45px 45px 0; + font-size: 30px; + font-weight: 800; + border: none; + margin: 0; + background: linear-gradient(135deg, #00b96b 0%, #00f0ff 100%); + color: #000; // Black text for high contrast on neon + display: flex; + align-items: center; + justify-content: center; + animation: pulse-glow 3s infinite; + transition: all 0.2s ease; + + &:active { + transform: scale(0.98); + opacity: 0.9; + } + + .cart-icon { + font-size: 36px; + margin-right: 12px; + } } } } .safe-area-bottom { - padding-bottom: calc(20px + constant(safe-area-inset-bottom)); - padding-bottom: calc(20px + env(safe-area-inset-bottom)); + padding-bottom: 0; } diff --git a/miniprogram/src/pages/goods/detail.tsx b/miniprogram/src/pages/goods/detail.tsx index dfc6b67..0609e85 100644 --- a/miniprogram/src/pages/goods/detail.tsx +++ b/miniprogram/src/pages/goods/detail.tsx @@ -2,6 +2,8 @@ import { View, Text, Image, ScrollView, Button } from '@tarojs/components' import Taro, { useRouter, useLoad } from '@tarojs/taro' import { useState } from 'react' import { getConfigDetail } from '../../api' +import ParticleBackground from '../../components/ParticleBackground' +import { addToCart } from '../../utils/cart' import './detail.scss' export default function Detail() { @@ -26,6 +28,11 @@ export default function Detail() { } } + const handleAddToCart = () => { + if (!product) return + addToCart(product) + } + const buyNow = () => { if (!product) return Taro.navigateTo({ @@ -38,12 +45,13 @@ export default function Detail() { return ( + {/* Hero Section */} - {product.detail_image_url || product.static_image_url ? ( - + {product.static_image_url ? ( + ) : ( @@ -65,44 +73,84 @@ export default function Detail() { {/* Stats Section */} - - - 售价 - ¥{product.price} + + + 售价 + 库存 - - - 库存 - {product.stock}件 + + + ¥ + {product.price} + + + {product.stock} + + {/* Features Section */} {product.features && product.features.length > 0 ? ( - product.features.map((f, idx) => ( - - - {f.icon_url ? : } + product.features.map((f, idx) => { + let iconContent + if (f.display_icon) { + iconContent = + } else if (f.icon_url) { + iconContent = + } 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 = {iconChar} + } + + return ( + + + {iconContent} + + + {f.title} + {f.description} + - - {f.title} - {f.description} - - - )) + ) + }) ) : ( - - 极致性能释放 - {product.chip_type} 强劲核心,提供强大的边缘计算算力支持。 - + <> + + + 🛡 + + + 工业级安全标准 + 采用军工级加密芯片,保障您的数据隐私安全。无论是边缘计算还是云端同步,全程加密传输。 + + + + + 👁 + + + 超清视觉感知 + 搭载 4K 高清摄像头与 AI 视觉算法,实时捕捉每一个细节。支持人脸识别、物体检测等。 + + + )} {/* Detail Image */} - {product.detail_image_url && ( + {(product.display_detail_image || product.detail_image_url) && ( - + )} @@ -110,14 +158,15 @@ export default function Detail() { {/* Bottom Bar */} - + - Taro.switchTab({ url: '/pages/cart/cart' })}> - 🛒 - 购物车 - - - + + diff --git a/miniprogram/src/pages/index/index.scss b/miniprogram/src/pages/index/index.scss index 4bb7997..b6c63f0 100644 --- a/miniprogram/src/pages/index/index.scss +++ b/miniprogram/src/pages/index/index.scss @@ -1,52 +1,79 @@ .page-container { height: 100vh; - background-color: #000; - color: #fff; + background-color: var(--bg-dark); + color: var(--text-main); overflow: hidden; position: relative; + + // Ambient Light 1 (Cyan) + &::before { + content: ''; + position: absolute; + top: -10%; + left: -10%; + width: 60%; + height: 40%; + background: radial-gradient(circle, rgba(0, 240, 255, 0.15) 0%, transparent 70%); + filter: blur(80px); + z-index: 0; + pointer-events: none; + } + + // Ambient Light 2 (Green/Purple mix) + &::after { + content: ''; + position: absolute; + bottom: 10%; + right: -10%; + width: 50%; + height: 40%; + background: radial-gradient(circle, rgba(189, 0, 255, 0.1) 0%, transparent 70%); + filter: blur(80px); + z-index: 0; + pointer-events: none; + } } .content-scroll { height: 100vh; position: relative; z-index: 1; - // Ensure no padding here } .scroll-inner { - // Container for scroll content width: 100%; } .header { text-align: center; - padding: 60px 20px 40px; + padding: 80px 24px 60px; // 增加头部留白 position: relative; .logo-box { - margin-bottom: 30px; + margin-bottom: 40px; display: flex; flex-direction: column; align-items: center; .logo-img { - width: 120px; - height: 120px; - margin-bottom: 15px; - filter: drop-shadow(0 0 15px rgba(0, 240, 255, 0.4)); + width: 140px; + height: 140px; + margin-bottom: 20px; + filter: drop-shadow(0 0 25px rgba(0, 240, 255, 0.5)); + animation: float 6s ease-in-out infinite; } .logo-text { - font-size: 40px; + font-size: 48px; font-weight: 900; color: #fff; - letter-spacing: 6px; - text-shadow: 0 0 20px rgba(0, 240, 255, 0.6); + letter-spacing: 8px; + text-shadow: 0 0 30px rgba(0, 240, 255, 0.7); } } .title-container { - margin-bottom: 25px; + margin-bottom: 30px; display: flex; justify-content: center; align-items: center; @@ -54,26 +81,27 @@ } .title-text { - font-size: 36px; - font-weight: bold; - color: #00f0ff; - text-shadow: 0 0 15px rgba(0, 240, 255, 0.5); + font-size: 40px; + font-weight: 800; + color: var(--primary-cyan); + text-shadow: 0 0 20px rgba(0, 240, 255, 0.4); } .cursor { - font-size: 36px; + font-size: 40px; color: #fff; margin-left: 8px; animation: blink 1s infinite; } .subtitle { - color: #aaa; - font-size: 26px; - line-height: 1.6; + color: var(--text-secondary); + font-size: 28px; + line-height: 1.8; // 增加行高 display: block; padding: 0 40px; - font-weight: 300; + font-weight: 400; + letter-spacing: 1px; } } @@ -82,40 +110,54 @@ 50% { opacity: 0; } } -.status-box { - padding: 100px 0; - text-align: center; - - .loading-text { color: #00f0ff; font-size: 28px; } - .error-text { color: #ff4d4f; font-size: 28px; margin-bottom: 20px; display: block;} - .btn-retry { background: rgba(255,255,255,0.1); color: #fff; font-size: 24px; padding: 10px 40px; display: inline-block;} +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } } .product-grid { - padding: 0 30px; + padding: 0 32px; display: flex; flex-direction: column; - gap: 40px; + gap: 48px; // 增加卡片间距 } +// 玻璃态卡片升级版 .card { background: rgba(255, 255, 255, 0.03); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border-radius: 32px; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); - transition: all 0.3s ease; + box-shadow: + 0 20px 40px rgba(0, 0, 0, 0.4), + inset 0 0 0 1px rgba(255, 255, 255, 0.05); // 内描边增强质感 + transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + position: relative; + // 高光反射效果 + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent); + opacity: 0.5; + } + &:active { - transform: scale(0.98); - border-color: #00b96b; - box-shadow: 0 0 30px rgba(0, 185, 107, 0.2); + transform: scale(0.96); + box-shadow: + 0 10px 20px rgba(0, 0, 0, 0.4), + 0 0 30px rgba(0, 240, 255, 0.1); // 按压发光 + border-color: rgba(0, 240, 255, 0.3); } &-cover { - height: 360px; + height: 400px; // 加大图片区域 background: #111; position: relative; overflow: hidden; @@ -123,7 +165,8 @@ .card-img { width: 100%; height: 100%; - transition: transform 0.5s ease; + object-fit: cover; + transition: transform 0.6s ease; } .placeholder-img { @@ -132,53 +175,85 @@ display: flex; align-items: center; justify-content: center; - background: radial-gradient(circle at center, #222, #111); - .icon-rocket { font-size: 100px; } - } + background: radial-gradient(circle at center, #1a1a1a, #050505); + + .radar-scan { + width: 100px; + height: 100px; + border: 2px solid rgba(0, 240, 255, 0.3); + border-radius: 50%; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 10px; + height: 10px; + background: var(--primary-cyan); + border-radius: 50%; + box-shadow: 0 0 10px var(--primary-cyan); + } + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 50%; + background: conic-gradient(from 0deg, transparent 0%, transparent 60%, rgba(0, 240, 255, 0.4) 100%); + animation: radar-spin 2s linear infinite; + } + } + } + .card-overlay { position: absolute; bottom: 0; left: 0; width: 100%; - height: 50%; - background: linear-gradient(to top, rgba(0,0,0,0.8), transparent); + height: 60%; + background: linear-gradient(to top, rgba(0,0,0,0.9), transparent); } } &-body { - padding: 30px; + padding: 40px 32px; } &-header { display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: 15px; + margin-bottom: 20px; .card-title { - font-size: 36px; - font-weight: bold; + font-size: 40px; // 加大标题 + font-weight: 700; color: #fff; flex: 1; margin-right: 20px; - line-height: 1.3; - text-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + line-height: 1.2; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); } .price { font-size: 36px; - color: #00b96b; - font-weight: 900; - text-shadow: 0 0 15px rgba(0, 185, 107, 0.3); + color: var(--primary-cyan); // 统一用青色或根据产品类型变化 + font-weight: 800; + text-shadow: 0 0 20px rgba(0, 240, 255, 0.3); } } &-desc { font-size: 26px; - color: #ccc; - line-height: 1.5; - margin-bottom: 25px; + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: 32px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; @@ -188,52 +263,126 @@ .tags { display: flex; flex-wrap: wrap; - gap: 12px; - margin-bottom: 30px; + gap: 16px; + margin-bottom: 40px; .tag { - padding: 8px 18px; - border-radius: 12px; + padding: 10px 24px; + border-radius: 16px; font-size: 22px; font-weight: 500; + letter-spacing: 0.5px; &.cyan { - color: #00f0ff; - background: rgba(0, 240, 255, 0.1); - border: 1px solid rgba(0, 240, 255, 0.3); + color: var(--primary-cyan); + background: rgba(0, 240, 255, 0.08); + border: 1px solid rgba(0, 240, 255, 0.2); } &.blue { color: #3b82f6; - background: rgba(59, 130, 246, 0.1); - border: 1px solid rgba(59, 130, 246, 0.3); + background: rgba(59, 130, 246, 0.08); + border: 1px solid rgba(59, 130, 246, 0.2); } &.purple { - color: #a855f7; - background: rgba(168, 85, 247, 0.1); - border: 1px solid rgba(168, 85, 247, 0.3); + color: var(--primary-purple); + background: rgba(189, 0, 255, 0.08); + border: 1px solid rgba(189, 0, 255, 0.2); } } } &-footer { .btn-buy { - background: linear-gradient(90deg, #00b96b, #00f0ff); + background: linear-gradient(90deg, var(--primary-green), var(--primary-cyan)); color: #000; - font-weight: bold; + font-weight: 800; font-size: 30px; - border-radius: 50px; + border-radius: 60px; // 更圆润 border: none; - height: 80px; - line-height: 80px; - box-shadow: 0 5px 20px rgba(0, 185, 107, 0.3); + height: 90px; + line-height: 90px; + box-shadow: 0 10px 30px rgba(0, 185, 107, 0.25); + position: relative; + overflow: hidden; + // 流光效果 + &::after { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); + animation: shimmer 3s infinite; + } + &:active { - opacity: 0.9; + transform: scale(0.98); + box-shadow: 0 5px 15px rgba(0, 185, 107, 0.2); } } } } -.footer-spacer { - height: 100px; +@keyframes shimmer { + 0% { left: -100%; } + 20% { left: 100%; } + 100% { left: 100%; } +} + +@keyframes radar-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.footer-spacer { + height: 120px; +} + +// 骨架屏样式 +.skeleton-wrapper { + padding: 0 32px; + display: flex; + flex-direction: column; + gap: 48px; +} + +.skeleton-card { + height: 700px; + background: rgba(255, 255, 255, 0.02); + border-radius: 32px; + border: 1px solid rgba(255, 255, 255, 0.05); + overflow: hidden; + position: relative; + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.03), transparent); + animation: skeleton-loading 1.5s infinite; + } +} + +@keyframes skeleton-loading { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +// 列表入场动画 +.fade-in-up { + animation: fadeInUp 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards; + opacity: 0; + transform: translateY(40px); +} + +@keyframes fadeInUp { + to { + opacity: 1; + transform: translateY(0); + } } diff --git a/miniprogram/src/pages/index/index.tsx b/miniprogram/src/pages/index/index.tsx index d8c913a..337d332 100644 --- a/miniprogram/src/pages/index/index.tsx +++ b/miniprogram/src/pages/index/index.tsx @@ -65,8 +65,10 @@ export default function Index() { {loading ? ( - - 正在加载硬件配置... + + {[1, 2, 3].map(i => ( + + ))} ) : error ? ( @@ -75,14 +77,19 @@ export default function Index() { ) : ( - {products.map((item) => ( - goToDetail(item.id)}> + {products.map((item, index) => ( + goToDetail(item.id)} + > {item.static_image_url ? ( ) : ( - 🚀 + )} diff --git a/miniprogram/src/pages/order/checkout.scss b/miniprogram/src/pages/order/checkout.scss index 9fdc11c..ee1e99d 100644 --- a/miniprogram/src/pages/order/checkout.scss +++ b/miniprogram/src/pages/order/checkout.scss @@ -1,51 +1,154 @@ .page-container { min-height: 100vh; - background-color: #f7f8fa; - padding: 15px; - padding-bottom: 80px; + background-color: #050505; + color: #fff; + padding-bottom: 120px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; } .section { - background: #fff; - border-radius: 12px; - padding: 20px; - margin-bottom: 15px; - box-shadow: 0 2px 8px rgba(0,0,0,0.02); + margin: 20px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 16px; + padding: 24px; + backdrop-filter: blur(10px); + position: relative; + + .section-title { + font-size: 28px; + font-weight: bold; + color: #fff; + margin-bottom: 20px; + display: block; + + &::before { + content: ''; + display: inline-block; + width: 6px; + height: 24px; + background: #00b96b; + margin-right: 12px; + vertical-align: middle; + border-radius: 3px; + } + } +} + +.delivery-type-section { + display: flex; + padding: 10px; + gap: 10px; + + .type-item { + flex: 1; + text-align: center; + padding: 16px 0; + font-size: 28px; + color: #888; + border-radius: 10px; + transition: all 0.3s; + + &.active { + background: #00b96b; + color: #fff; + font-weight: bold; + } + } } .address-section { - min-height: 80px; display: flex; - flex-direction: column; - justify-content: center; - - .row { - margin-bottom: 8px; - .name { font-size: 16px; font-weight: bold; margin-right: 10px; } - .phone { font-size: 14px; color: #666; } - } - .addr { font-size: 14px; color: #333; line-height: 1.4; } + align-items: center; + justify-content: space-between; - .placeholder-container { - display: flex; - justify-content: center; - align-items: center; - height: 100%; + .address-info { + flex: 1; + .user-info { + font-size: 30px; + font-weight: bold; + margin-bottom: 8px; + .phone { margin-left: 20px; color: #888; font-weight: normal; font-size: 26px; } + } + .address-text { + font-size: 26px; + color: #aaa; + line-height: 1.4; + } + .placeholder { + color: #00b96b; + font-size: 30px; + font-weight: bold; + } + } + + .arrow { + font-size: 30px; + color: #666; + margin-left: 20px; } - .placeholder { font-size: 16px; color: #00b96b; } } .product-section { - .p-name { font-size: 16px; font-weight: 500; margin-bottom: 10px; display: block; } - .row { display: flex; justify-content: space-between; align-items: center; } - .p-price { font-size: 16px; color: #333; } - .p-qty { font-size: 14px; color: #999; } - - .divider { height: 1px; background: #eee; margin: 15px 0; } - - .total-row { - .total-price { font-size: 20px; color: #ff4d4f; font-weight: bold; } - } + padding: 0; // Remove padding for list + overflow: hidden; + + .section-title { margin: 24px 24px 10px; } + + .product-item { + display: flex; + padding: 20px 24px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + + &:last-child { border-bottom: none; } + + .p-img { + width: 120px; + height: 120px; + border-radius: 8px; + background: #000; + margin-right: 20px; + object-fit: cover; + } + + .p-info { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + + .p-name { font-size: 28px; color: #fff; font-weight: bold; } + .p-desc { font-size: 24px; color: #888; } + + .p-meta { + display: flex; + justify-content: space-between; + align-items: center; + .p-price { font-size: 30px; color: #00b96b; font-weight: bold; } + .p-qty { font-size: 26px; color: #888; } + } + } + } +} + +.summary-section { + .row { + display: flex; + justify-content: space-between; + margin-bottom: 16px; + font-size: 28px; + color: #888; + + &.total { + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + color: #fff; + font-weight: bold; + font-size: 32px; + .price { color: #00b96b; font-size: 40px; } + } + } } .bottom-bar { @@ -53,22 +156,36 @@ bottom: 0; left: 0; right: 0; - background: #fff; - padding: 10px 20px; - border-top: 1px solid #eee; + height: 110px; + background: rgba(20, 20, 20, 0.95); + backdrop-filter: blur(20px); + border-top: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: flex-end; + padding: 0 30px; + z-index: 100; + + .total-label { font-size: 28px; color: #fff; margin-right: 20px; } + .total-price { font-size: 40px; color: #00b96b; font-weight: bold; margin-right: 30px; } .btn-submit { - background: #00b96b; - color: #fff; - border-radius: 22px; + background: linear-gradient(135deg, #00b96b 0%, #00f0ff 100%); + color: #000; + border-radius: 40px; + padding: 0 60px; + height: 80px; + line-height: 80px; + font-size: 32px; + font-weight: bold; border: none; - font-size: 16px; - height: 44px; - line-height: 44px; + box-shadow: 0 0 20px rgba(0, 185, 107, 0.3); + + &:active { transform: scale(0.98); } + &.disabled { + background: #333; + color: #666; + box-shadow: none; + } } } - -.safe-area-bottom { - padding-bottom: constant(safe-area-inset-bottom); - padding-bottom: env(safe-area-inset-bottom); -} diff --git a/miniprogram/src/pages/order/checkout.tsx b/miniprogram/src/pages/order/checkout.tsx index 4d14116..34f4aa6 100644 --- a/miniprogram/src/pages/order/checkout.tsx +++ b/miniprogram/src/pages/order/checkout.tsx @@ -1,98 +1,220 @@ -import { View, Text, Button } from '@tarojs/components' +import { View, Text, Image, ScrollView, Button } from '@tarojs/components' import Taro, { useRouter, useLoad } from '@tarojs/taro' -import { useState } from 'react' +import { useState, useMemo } from 'react' import { getConfigDetail, createOrder } from '../../api' +import { getSelectedItems, removeItem } from '../../utils/cart' import './checkout.scss' export default function Checkout() { const router = useRouter() - const { id, quantity } = router.params - const [product, setProduct] = useState(null) + const params = router.params + const [items, setItems] = useState([]) const [address, setAddress] = useState(null) - const [contact, setContact] = useState({ name: '', phone: '' }) + const [deliveryType, setDeliveryType] = useState<'delivery' | 'pickup'>('delivery') + const [userAddress, setUserAddress] = useState(null) + const [loading, setLoading] = useState(true) + + const PICKUP_ADDRESS = { + userName: '云南量迹科技有限公司', + telNumber: '18585164448', + provinceName: '云南省', + cityName: '昆明市', + countyName: '西山区', + detailInfo: '永昌街道办事处云纺国际商厦 B 座 1406 号' + } useLoad(async () => { - if (id) { - const res = await getConfigDetail(Number(id)) - setProduct(res) + if (params.from === 'cart') { + const cartItems = getSelectedItems() + if (cartItems.length === 0) { + Taro.navigateBack() + return + } + setItems(cartItems) + setLoading(false) + } else if (params.id) { + try { + const res = await getConfigDetail(params.id) + setItems([{ + id: res.id, + name: res.name, + price: res.price, + image: res.static_image_url || res.detail_image_url, + quantity: Number(params.quantity) || 1, + description: res.description + }]) + } catch (err) { + console.error(err) + Taro.showToast({ title: '商品加载失败', icon: 'none' }) + } finally { + setLoading(false) + } } }) const chooseAddress = async () => { + if (deliveryType === 'pickup') return try { - const res = await Taro.chooseAddress() - setAddress(res) - setContact({ name: res.userName, phone: res.telNumber }) - } catch (e) { - Taro.showToast({ title: '需要授权获取地址', icon: 'none' }) + const res = await Taro.chooseAddress() + setAddress(res) + setUserAddress(res) + } catch (err) { + console.error(err) + // User cancelled or auth denied } } + const handleTypeChange = (type: 'delivery' | 'pickup') => { + if (type === deliveryType) return + setDeliveryType(type) + if (type === 'pickup') { + setAddress(PICKUP_ADDRESS) + } else { + setAddress(userAddress) + } + } + + const totalPrice = useMemo(() => { + return items.reduce((sum, item) => sum + item.price * item.quantity, 0) + }, [items]) + const submitOrder = async () => { if (!address) { - Taro.showToast({ title: '请选择收货地址', icon: 'none' }) - return + Taro.showToast({ title: '请选择收货地址', icon: 'none' }) + return } - + + Taro.showLoading({ title: '提交中...' }) + try { - Taro.showLoading({ title: '正在下单...' }) + const orderPromises = items.map(item => { const orderData = { - goodid: product.id, - quantity: Number(quantity || 1), - customer_name: contact.name, - phone_number: contact.phone, - shipping_address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}`, - // ref_code: Taro.getStorageSync('ref_code') - } - - const res = await createOrder(orderData) - Taro.hideLoading() - - if (res.order_id) { - Taro.redirectTo({ url: `/pages/order/payment?id=${res.order_id}` }) + goodid: item.id, + quantity: item.quantity, + customer_name: address.userName, + phone_number: address.telNumber, + shipping_address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}` } + return createOrder(orderData) + }) + + const results = await Promise.all(orderPromises) + + // If from cart, remove bought items + if (params.from === 'cart') { + items.forEach(item => removeItem(item.id)) + } + + Taro.hideLoading() + + if (results.length === 1) { + // Single order, go to payment + const orderId = results[0].order_id + Taro.redirectTo({ + url: `/pages/order/payment?id=${orderId}` + }) + } else { + // Multiple orders + Taro.showModal({ + title: '下单成功', + content: `成功创建 ${results.length} 个订单,请前往订单列表支付`, + showCancel: false, + confirmText: '去支付', + success: () => { + Taro.redirectTo({ url: '/pages/order/list' }) + } + }) + } + } catch (err) { - Taro.hideLoading() - console.error(err) + Taro.hideLoading() + console.error(err) + Taro.showToast({ title: '下单失败', icon: 'none' }) } } - if (!product) return Loading... + if (loading) return Loading... return ( - + + + {/* Delivery Type Section */} + + handleTypeChange('delivery')} + > + 快递配送 + + handleTypeChange('pickup')} + > + 门店自提 + + + + {/* Address Section */} {address ? ( - - - {contact.name} - {contact.phone} + + + {address.userName} + {address.telNumber} + + + {address.provinceName}{address.cityName}{address.countyName}{address.detailInfo} - {address.provinceName}{address.cityName}{address.countyName}{address.detailInfo} ) : ( - - + 选择收货地址 + + + 添加收货地址 )} + {deliveryType === 'delivery' && } + {/* Products Section */} - {product.name} - - ¥{product.price} - x {quantity} - - - - 合计 - ¥{(product.price * (Number(quantity) || 1)).toFixed(2)} - + 商品信息 + {items.map((item, idx) => ( + + + + {item.name} + {item.description} + + ¥{item.price} + x{item.quantity} + + + + ))} - - + {/* Summary Section */} + + + 商品总价 + ¥{totalPrice} + + + 运费 + ¥0 + + + 合计 + ¥{totalPrice} + - + + + {/* Bottom Bar */} + + 共{items.length}件 + ¥{totalPrice} + + + ) } diff --git a/miniprogram/src/pages/services/index.scss b/miniprogram/src/pages/services/index.scss index 25f8476..d3d5fe6 100644 --- a/miniprogram/src/pages/services/index.scss +++ b/miniprogram/src/pages/services/index.scss @@ -128,59 +128,158 @@ .process-section { margin-top: 60px; padding: 40px 20px; - background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,185,107,0.05) 100%); - border-radius: 30px; - border: 1px solid rgba(255,255,255,0.05); + position: relative; + overflow: hidden; + // Background Tech Grid + &::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; + background-image: + linear-gradient(rgba(0, 185, 107, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 185, 107, 0.03) 1px, transparent 1px); + background-size: 20px 20px; + z-index: 0; + } + .section-title { color: #fff; text-align: center; font-size: 36px; font-weight: bold; - margin-bottom: 40px; + margin-bottom: 60px; display: block; - text-shadow: 0 0 10px rgba(0, 185, 107, 0.5); + text-shadow: 0 0 15px rgba(0, 185, 107, 0.8); + position: relative; + z-index: 1; + letter-spacing: 2px; + + &::after { + content: ''; + display: block; + width: 60px; + height: 4px; + background: #00b96b; + margin: 15px auto 0; + border-radius: 2px; + box-shadow: 0 0 10px #00b96b; + } } .process-steps { display: flex; - flex-wrap: wrap; - justify-content: space-between; - gap: 20px; + flex-direction: column; + position: relative; + z-index: 1; + padding: 0 20px; + + // Vertical connecting line + &::before { + content: ''; + position: absolute; + top: 20px; + bottom: 20px; + left: 60px; // Center of the icon (40px + padding) + width: 2px; + background: rgba(255, 255, 255, 0.1); + z-index: 0; + } + + // Moving signal on the line + &::after { + content: ''; + position: absolute; + top: 20px; + left: 60px; + width: 2px; + height: 100px; + background: linear-gradient(to bottom, transparent, #00b96b, transparent); + animation: signalFlow 3s infinite linear; + z-index: 0; + } } .step-item { - width: 48%; // 2 columns display: flex; - flex-direction: column; align-items: center; - margin-bottom: 30px; + margin-bottom: 40px; + position: relative; + + &:last-child { margin-bottom: 0; } .step-icon { width: 80px; height: 80px; - border-radius: 24px; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(0, 185, 107, 0.3); + border-radius: 20px; + background: rgba(0, 0, 0, 0.6); + border: 1px solid rgba(0, 185, 107, 0.5); display: flex; align-items: center; justify-content: center; - margin-bottom: 15px; color: #00b96b; - font-size: 32px; + font-size: 36px; font-weight: bold; + margin-right: 30px; + position: relative; + z-index: 1; + box-shadow: 0 0 15px rgba(0, 185, 107, 0.2); + transition: all 0.3s ease; + + // Pulse effect for icon + &::before { + content: ''; + position: absolute; + top: -5px; bottom: -5px; left: -5px; right: -5px; + border-radius: 24px; + border: 1px solid rgba(0, 185, 107, 0.3); + animation: pulseBorder 2s infinite; + } } - .step-title { - color: #fff; - font-size: 28px; - font-weight: bold; - margin-bottom: 5px; - } - - .step-desc { - color: #666; - font-size: 24px; + // Content Card + .step-content-wrapper { + flex: 1; + background: linear-gradient(90deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%); + border: 1px solid rgba(255, 255, 255, 0.05); + border-left: 4px solid #00b96b; + padding: 20px 24px; + border-radius: 0 16px 16px 0; + backdrop-filter: blur(5px); + transform: translateX(0); + transition: all 0.3s ease; + + &:active { + background: rgba(255, 255, 255, 0.05); + transform: translateX(5px); + } + + .step-title { + color: #fff; + font-size: 30px; + font-weight: bold; + margin-bottom: 8px; + display: block; + text-shadow: 0 0 5px rgba(0,0,0,0.5); + } + + .step-desc { + color: #888; + font-size: 24px; + line-height: 1.4; + } } } } + +@keyframes signalFlow { + 0% { top: 0; opacity: 0; } + 20% { opacity: 1; } + 80% { opacity: 1; } + 100% { top: 100%; opacity: 0; } +} + +@keyframes pulseBorder { + 0% { transform: scale(1); opacity: 0.5; } + 100% { transform: scale(1.15); opacity: 0; } +} diff --git a/miniprogram/src/pages/services/index.tsx b/miniprogram/src/pages/services/index.tsx index fec453c..5711a30 100644 --- a/miniprogram/src/pages/services/index.tsx +++ b/miniprogram/src/pages/services/index.tsx @@ -91,8 +91,10 @@ export default function ServicesIndex() { ].map((step) => ( {step.id} - {step.title} - {step.desc} + + {step.title} + {step.desc} + ))} diff --git a/miniprogram/src/pages/user/index.scss b/miniprogram/src/pages/user/index.scss index fbd1d2e..56fc6fb 100644 --- a/miniprogram/src/pages/user/index.scss +++ b/miniprogram/src/pages/user/index.scss @@ -1,53 +1,207 @@ .page-container { min-height: 100vh; - background-color: #f7f8fa; + background-color: #050505; + color: #fff; + padding: 30px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; } -.header { - background: #fff; - padding: 40px 20px; +@keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(0, 185, 107, 0.4); } + 70% { box-shadow: 0 0 0 10px rgba(0, 185, 107, 0); } + 100% { box-shadow: 0 0 0 0 rgba(0, 185, 107, 0); } +} + +@keyframes float { + 0% { transform: translateY(0); } + 50% { transform: translateY(-5px); } + 100% { transform: translateY(0); } +} + +.profile-card { + background: linear-gradient(135deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%); + border: 1px solid rgba(255,255,255,0.05); + backdrop-filter: blur(20px); + border-radius: 20px; + padding: 40px; display: flex; align-items: center; - margin-bottom: 20px; + margin-bottom: 30px; + position: relative; + overflow: hidden; - .avatar { - width: 60px; - height: 60px; - border-radius: 30px; - margin-right: 15px; - background: #eee; + .card-bg-effect { + position: absolute; + top: -50%; + right: -20%; + width: 200px; + height: 200px; + background: radial-gradient(circle, rgba(0, 185, 107, 0.2) 0%, transparent 70%); + filter: blur(40px); + z-index: 0; } - - .nickname { - font-size: 18px; - font-weight: bold; - color: #333; + + .avatar-container { + position: relative; + margin-right: 30px; + z-index: 1; + + .avatar { + width: 120px; + height: 120px; + border-radius: 60px; + border: 2px solid rgba(0, 185, 107, 0.5); + background: #000; + } + + .online-dot { + position: absolute; + bottom: 5px; + right: 5px; + width: 24px; + height: 24px; + background: #00b96b; + border-radius: 50%; + border: 3px solid #111; + animation: pulse 2s infinite; + } + } + + .info-col { + flex: 1; + z-index: 1; + display: flex; + flex-direction: column; + + .nickname { + font-size: 36px; + font-weight: bold; + color: #fff; + margin-bottom: 8px; + text-shadow: 0 0 10px rgba(0,0,0,0.5); + } + + .uid { + font-size: 24px; + color: #666; + margin-bottom: 20px; + font-family: monospace; + } + + .btn-login { + background: rgba(0, 185, 107, 0.2); + border: 1px solid #00b96b; + color: #00b96b; + font-size: 24px; + border-radius: 30px; + padding: 0 30px; + height: 60px; + line-height: 58px; + margin: 0; + width: fit-content; + + &:active { background: rgba(0, 185, 107, 0.3); } + } } } -.menu { - background: #fff; - - .item { - padding: 15px 20px; - border-bottom: 1px solid #eee; +.stats-row { display: flex; justify-content: space-between; - align-items: center; - font-size: 16px; - position: relative; + margin-bottom: 30px; + padding: 0 10px; - &:last-child { border-bottom: none; } - - .arrow { color: #ccc; } - - .btn-contact { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0; + .stat-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + + .stat-val { font-size: 36px; font-weight: bold; color: #fff; margin-bottom: 5px; } + .stat-lbl { font-size: 24px; color: #666; } + } +} + +.service-container { + padding-bottom: 40px; + + .service-group { + margin-bottom: 40px; + + .group-title { + display: block; + font-size: 32px; + font-weight: bold; + color: #fff; + margin-bottom: 20px; + padding-left: 10px; + border-left: 4px solid #00b96b; + line-height: 1; + } + + .grid-layout { + display: flex; + flex-wrap: wrap; + gap: 20px; + + .grid-item { + width: calc(33.33% - 14px); // 3 items per row, accounting for gap + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 20px; + padding: 30px 10px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + box-sizing: border-box; + backdrop-filter: blur(10px); + transition: all 0.2s ease; + + &:active { + background: rgba(255, 255, 255, 0.08); + transform: scale(0.95); + } + + .icon-box { + width: 80px; + height: 80px; + border-radius: 50%; + background: rgba(0, 185, 107, 0.1); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; + + .icon { font-size: 40px; } + } + + .item-title { + font-size: 26px; + color: #ddd; + text-align: center; + } + + .contact-overlay { + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; + opacity: 0; + } + } + } + } +} + +.version-info { + margin-top: 60px; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + + text { + font-size: 20px; + color: #333; } - } } diff --git a/miniprogram/src/pages/user/index.tsx b/miniprogram/src/pages/user/index.tsx index a4b6cd5..76c3ebe 100644 --- a/miniprogram/src/pages/user/index.tsx +++ b/miniprogram/src/pages/user/index.tsx @@ -13,33 +13,99 @@ export default function UserIndex() { const goOrders = () => Taro.navigateTo({ url: '/pages/order/list' }) const goDistributor = () => Taro.navigateTo({ url: '/subpackages/distributor/index' }) + const goInvite = () => Taro.navigateTo({ url: '/subpackages/distributor/invite' }) + const goWithdraw = () => Taro.navigateTo({ url: '/subpackages/distributor/withdraw' }) + + const handleAddress = async () => { + try { await Taro.chooseAddress() } catch(e) {} + } + const login = () => { - // Trigger login again if needed Taro.reLaunch({ url: '/pages/index/index' }) } + const serviceGroups = [ + { + title: '基础服务', + items: [ + { title: '我的订单', icon: '📦', action: goOrders }, + { title: '地址管理', icon: '📍', action: handleAddress }, + { title: '新增地址', icon: '📝', action: handleAddress }, + ] + }, + { + title: '分销中心', + items: [ + { title: '分销首页', icon: '⚡', action: goDistributor }, + { title: '推广邀请', icon: '🤝', action: goInvite }, + { title: '佣金提现', icon: '💰', action: goWithdraw }, + ] + }, + { + title: '其他', + items: [ + { title: '联系客服', icon: '🎧', isContact: true } + ] + } + ] + + const stats = [ + { label: '余额', value: '0.00' }, + { label: '积分', value: '0' }, + { label: '优惠券', value: '0' } + ] + return ( - - - {userInfo?.nickname || '未登录'} - {!userInfo && } + {/* Profile Card */} + + + + {userInfo && } + + + {userInfo?.nickname || '未登录用户'} + ID: {userInfo ? '888888' : '----'} + {!userInfo && ( + + )} + + - - - 我的订单 - > - - - 分销中心 - > - - - 联系客服 - - > - + {/* Stats Row */} + + {stats.map((item, idx) => ( + + {item.value} + {item.label} + + ))} + + + {/* Service Groups */} + + {serviceGroups.map((group, gIdx) => ( + + {group.title} + + {group.items.map((item, idx) => ( + + + {item.icon} + + {item.title} + {item.isContact && } + + ))} + + + ))} + + + + Quant Speed Market v1.0.0 + Powered by Taro & React ) diff --git a/miniprogram/src/subpackages/distributor/index.tsx b/miniprogram/src/subpackages/distributor/index.tsx index a8902d8..a1aadb9 100644 --- a/miniprogram/src/subpackages/distributor/index.tsx +++ b/miniprogram/src/subpackages/distributor/index.tsx @@ -30,6 +30,7 @@ export default function DistributorIndex() { const goInvite = () => Taro.navigateTo({ url: '/subpackages/distributor/invite' }) const goWithdraw = () => Taro.navigateTo({ url: '/subpackages/distributor/withdraw' }) + const showComingSoon = () => Taro.showToast({ title: '功能开发中', icon: 'none' }) if (loading) return Loading... if (!info) return Error @@ -62,11 +63,11 @@ export default function DistributorIndex() { 推广二维码 > - + 我的团队 > - + 提现记录 > diff --git a/miniprogram/src/utils/cart.ts b/miniprogram/src/utils/cart.ts new file mode 100644 index 0000000..05f605b --- /dev/null +++ b/miniprogram/src/utils/cart.ts @@ -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) +}