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

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

View File

@@ -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"
},
{

View File

@@ -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 {

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">
<path d="M49.4996 16.5C49.4996 7.38729 42.1123 0 32.9996 0C23.8869 0 16.4996 7.38729 16.4996 16.5H49.4996Z" fill="white"/>
<path d="M33 66C42.1127 66 49.5 58.6127 49.5 49.5H16.5C16.5 58.6127 23.8873 66 33 66Z" fill="white"/>
<path d="M66 33C66 42.1127 58.6127 49.5 49.5 49.5V16.5C58.6127 16.5 66 23.8873 66 33Z" fill="white"/>
<path d="M0 33C0 23.8873 7.3873 16.5 16.5 16.5V49.5C7.3873 49.5 0 42.1127 0 33Z" fill="white"/>
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="300.000000pt" height="198.000000pt" viewBox="0 0 300.000000 198.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.10, written by Peter Selinger 2001-2011
</metadata>
<g transform="translate(0.000000,198.000000) scale(0.050000,-0.050000)"
fill="#FFFFFF" stroke="none">
<path d="M1836 3803 c-149 -150 -196 -223 -196 -304 0 -58 24 -46 85 42 95
137 365 284 404 220 17 -27 127 -410 120 -417 -3 -4 -43 21 -90 55 -108 78
-289 58 -349 -38 -27 -45 9 -46 101 -2 225 105 249 -11 66 -313 -41 -68 -99
-170 -127 -227 -47 -91 -58 -101 -102 -90 -78 19 -88 -3 -48 -95 21 -47 44
-125 52 -172 29 -184 162 -172 247 23 25 58 98 213 161 345 63 132 121 267
129 300 18 75 31 60 153 -178 208 -401 479 -647 755 -684 106 -14 223 32 223
87 0 64 -209 203 -307 205 -115 1 -389 222 -556 447 -182 243 -255 595 -117
559 49 -13 68 30 43 96 -28 71 -174 218 -217 218 -20 0 -86 18 -146 40 -60 22
-114 40 -120 39 -5 0 -79 -70 -164 -156z"/>
<path d="M5153 3827 c-24 -25 -13 -47 23 -47 98 0 112 -112 22 -178 -50 -37
-78 -44 -140 -34 -100 16 -109 -43 -18 -108 79 -56 76 -68 -29 -108 -115 -45
-191 -105 -191 -153 0 -45 18 -48 87 -17 59 27 142 -9 125 -54 -6 -15 -3 -50
7 -75 13 -35 9 -56 -16 -82 -31 -32 -29 -34 27 -28 118 11 61 -34 -136 -108
-121 -45 -234 -75 -288 -75 -104 0 -107 -30 -8 -87 l64 -38 177 83 c182 84
241 99 241 60 0 -53 -93 -181 -166 -229 -100 -67 -76 -108 43 -72 126 38 174
89 224 242 39 117 54 141 97 150 103 21 219 18 248 -6 59 -49 423 -22 430 32
6 37 -8 52 -70 76 -42 17 -98 41 -126 53 -27 13 -59 21 -70 18 -29 -8 -357
-61 -376 -62 -67 -1 7 94 95 123 145 48 141 75 -17 126 -35 11 -34 18 23 77
47 48 58 72 44 96 -22 40 30 93 114 116 83 22 78 60 -21 167 -70 76 -101 95
-155 96 -37 0 -89 13 -117 29 -53 30 -125 39 -147 17z m286 -115 c27 -33 -11
-144 -66 -189 -43 -36 -44 -1 -5 113 16 44 24 86 17 92 -6 7 -2 12 9 12 12 0
32 -13 45 -28z m-211 -378 c17 -27 10 -45 -33 -88 l-55 -55 -70 38 c-89 47
-88 54 15 106 95 47 113 47 143 -1z"/>
<path d="M1151 3392 c11 -60 8 -72 -21 -72 -19 0 -52 -22 -74 -50 -21 -27 -53
-50 -70 -50 -25 0 -26 5 -6 30 14 17 20 36 14 43 -7 7 -23 -14 -35 -45 -41
-103 -53 -118 -94 -118 -59 0 -75 -24 -27 -40 23 -7 43 -23 44 -36 6 -59 -8
-127 -31 -154 -41 -50 -62 -95 -30 -65 38 35 59 31 58 -10 0 -42 -96 -117
-118 -92 -109 123 -96 467 17 467 12 0 22 9 22 20 0 11 -12 20 -26 20 -14 0
-61 18 -105 40 -110 57 -138 41 -99 -53 17 -41 44 -147 59 -236 16 -88 43
-194 60 -235 43 -103 40 -114 -37 -146 -66 -28 -92 -63 -92 -125 0 -26 20 -29
128 -19 100 9 175 1 332 -37 281 -68 620 -39 620 53 0 44 -63 76 -112 57 -28
-10 -98 -19 -157 -19 l-106 0 -12 75 c-7 41 -13 130 -13 197 l0 123 85 31 c47
17 108 34 135 38 28 3 50 19 50 36 0 41 -124 79 -215 66 -85 -13 -115 59 -35
84 49 16 53 81 6 107 -19 11 -44 53 -55 94 -29 103 -79 120 -60 21z m9 -367
c0 -93 -110 -178 -146 -113 -17 31 -22 29 -46 -14 l-27 -48 0 54 c-1 34 12 57
35 65 21 6 63 38 94 71 75 78 90 76 90 -15z m0 -254 c0 -105 -24 -126 -91 -82
-74 48 -59 13 35 -83 106 -108 100 -113 -121 -83 -178 23 -183 26 -183 76 0
60 95 129 148 109 21 -8 37 2 48 31 14 38 111 116 149 120 8 0 15 -39 15 -88z"/>
<path d="M3842 3282 c-44 -28 -6 -62 70 -62 96 0 201 -48 218 -99 22 -69 -171
-359 -400 -601 -253 -267 -269 -338 -114 -505 71 -76 113 -69 184 30 34 47
107 147 163 223 309 424 418 676 349 808 -75 145 -365 272 -470 206z"/>
<path d="M5331 2779 c7 -12 75 -62 151 -110 84 -54 138 -103 138 -124 1 -68
198 -200 226 -150 55 98 13 173 -156 281 -125 79 -392 156 -359 103z"/>
<path d="M4240 2461 c0 -10 52 -59 116 -108 64 -49 147 -124 186 -166 170
-188 257 -247 361 -247 206 0 55 221 -243 355 -60 27 -173 80 -250 117 -148
73 -170 79 -170 49z"/>
<path d="M1145 2247 c-31 -13 -277 -299 -335 -392 -20 -31 -151 -222 -327
-475 -49 -71 -106 -148 -126 -170 -19 -23 -59 -86 -88 -140 -29 -55 -92 -151
-140 -213 -129 -165 -137 -196 -89 -356 83 -281 158 -274 309 29 63 127 149
268 192 315 42 47 87 109 99 137 21 49 34 53 186 65 90 7 223 18 295 25 118
12 130 9 131 -25 0 -20 7 -71 15 -114 14 -67 10 -77 -27 -87 -85 -22 67 -396
161 -396 l49 0 -1 240 c-1 340 -88 1041 -148 1198 -13 35 -17 104 -9 172 19
167 -33 233 -147 187z m44 -412 c16 -104 51 -542 43 -550 -4 -4 -82 -10 -175
-14 -92 -3 -187 -13 -212 -21 -67 -22 -57 16 43 178 48 78 94 164 102 192 19
61 155 280 174 280 8 0 19 -29 25 -65z"/>
<path d="M5212 1983 c-7 -11 15 -118 49 -237 161 -559 176 -476 -116 -627 -94
-49 -189 -113 -211 -143 -22 -30 -48 -50 -57 -44 -36 22 -14 79 65 173 266
315 280 517 46 634 -133 66 -203 42 -106 -37 65 -52 -40 -220 -319 -507 l-111
-114 34 -71 c19 -39 34 -83 34 -98 0 -60 78 -102 145 -76 80 30 80 29 52 -94
-15 -65 -17 -110 -6 -118 11 -8 37 -50 59 -94 82 -164 121 -122 113 120 -8
250 11 297 131 317 42 7 147 44 235 83 87 38 162 65 167 61 10 -10 79 -260
113 -411 14 -60 40 -145 58 -187 18 -43 33 -98 33 -122 0 -63 140 -310 201
-355 126 -93 150 -17 146 464 -5 576 -6 600 -27 600 -11 0 -20 -27 -20 -60 0
-71 -107 -415 -146 -468 -25 -34 -30 -29 -54 60 -14 54 -40 125 -57 158 -38
74 -122 328 -123 372 0 17 25 44 55 59 30 16 80 46 110 67 30 22 93 44 140 51
181 24 102 101 -155 150 -262 50 -273 59 -338 267 -54 174 -114 270 -140 227z
m-364 -559 c-75 -99 -310 -344 -331 -344 -9 0 71 103 178 228 180 211 282 288
153 116z m691 56 c102 -52 111 -69 52 -108 -75 -49 -103 -39 -128 45 -36 122
-37 121 76 63z"/>
<path d="M5560 1980 c0 -11 9 -20 19 -20 11 0 27 -29 37 -65 11 -46 36 -75 85
-98 131 -63 148 35 26 143 -66 58 -167 82 -167 40z"/>
<path d="M2490 1947 c-69 -16 -169 -24 -235 -18 -100 9 -115 6 -115 -24 1 -77
77 -125 201 -125 71 0 81 -5 69 -35 -8 -19 -19 -53 -25 -75 -23 -95 -197 -554
-265 -700 -99 -214 -106 -219 -298 -203 -170 15 -182 8 -182 -96 0 -89 28 -94
506 -84 446 9 534 23 534 85 0 66 -265 168 -337 129 -161 -86 -171 29 -20 242
20 29 37 71 37 93 0 44 72 216 166 394 39 75 63 150 63 200 l1 80 145 1 c165
1 212 26 158 85 -72 80 -205 97 -403 51z"/>
<path d="M3873 1627 c-7 -84 -13 -191 -13 -237 l0 -84 -135 -51 c-74 -28 -160
-57 -191 -64 -32 -7 -53 -23 -49 -37 14 -42 167 -39 267 5 51 22 96 41 101 41
4 0 7 -60 7 -132 l0 -133 -65 15 c-148 35 -275 -7 -275 -92 0 -32 5 -33 61 -4
62 31 169 24 244 -16 37 -21 48 -149 22 -277 -20 -100 -244 -43 -288 73 -11
28 -25 47 -30 42 -40 -41 318 -436 394 -436 65 0 77 88 77 569 l0 467 63 22
c35 12 89 22 120 22 94 0 77 73 -23 100 -99 27 -160 90 -160 164 0 214 -109
251 -127 43z"/>
<path d="M3063 1349 c-22 -28 -53 -94 -69 -145 -85 -278 -83 -254 -38 -350 77
-164 174 -184 245 -51 23 41 24 41 80 -21 95 -104 146 -23 147 238 2 292 -70
418 -194 341 -33 -21 -43 -20 -58 7 -26 46 -67 39 -113 -19z m237 -129 c0 -59
-8 -80 -31 -80 -17 0 -58 -14 -90 -31 -82 -42 -81 8 3 113 81 103 118 102 118
-2z m0 -167 c0 -70 -82 -137 -122 -101 -53 48 -47 81 17 105 78 28 105 27 105
-4z m-140 -132 c16 -32 13 -43 -20 -61 -36 -19 -40 -15 -40 39 0 69 29 80 60
22z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 526 B

After

Width:  |  Height:  |  Size: 6.8 KiB

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 {
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; }

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'
export default function Cart() {
const [cartItems, setCartItems] = useState<CartItem[]>([])
useDidShow(() => {
refreshCart()
})
const refreshCart = () => {
setCartItems(getCart())
}
const handleUpdateQuantity = (id: number, delta: number) => {
const item = cartItems.find(i => i.id === id)
if (!item) return
const newQty = item.quantity + delta
if (newQty < 1) return
const newCart = updateQuantity(id, newQty)
setCartItems(newCart)
}
const handleRemove = (id: number) => {
Taro.showModal({
title: '提示',
content: '确定要删除该商品吗?',
success: (res) => {
if (res.confirm) {
const newCart = removeItem(id)
setCartItems(newCart)
}
}
})
}
const handleToggle = (id: number) => {
const newCart = toggleSelect(id)
setCartItems(newCart)
}
const isAllSelected = useMemo(() => {
return cartItems.length > 0 && cartItems.every(i => i.selected)
}, [cartItems])
const handleToggleAll = () => {
const newCart = toggleSelectAll(!isAllSelected)
setCartItems(newCart)
}
const selectedCount = useMemo(() => {
return cartItems.filter(i => i.selected).reduce((sum, i) => sum + i.quantity, 0)
}, [cartItems])
const totalPrice = useMemo(() => {
return cartItems.filter(i => i.selected).reduce((sum, i) => sum + i.price * i.quantity, 0)
}, [cartItems])
const handleCheckout = () => {
if (selectedCount === 0) {
Taro.showToast({ title: '请选择商品', icon: 'none' })
return
}
Taro.navigateTo({
url: '/pages/order/checkout?from=cart'
})
}
const goShopping = () => {
Taro.switchTab({ url: '/pages/index/index' })
}
return (
<View className='page-container'>
<View className='empty'>
<Text>线</Text>
</View>
{cartItems.length === 0 ? (
<View className='empty-state'>
<Text className='empty-icon'>🛒</Text>
<Text className='empty-text'></Text>
<Button onClick={goShopping} style={{marginTop: 20, background: '#00b96b', color: '#fff'}}></Button>
</View>
) : (
<ScrollView scrollY className='cart-list'>
{cartItems.map(item => (
<View key={item.id} className='cart-item'>
<View className='checkbox-area' onClick={() => handleToggle(item.id)}>
<View className={`checkbox ${item.selected ? 'checked' : ''}`}>
{item.selected && <Text></Text>}
</View>
</View>
<Image src={item.image} className='item-img' mode='aspectFill' />
<View className='item-info'>
<View>
<Text className='item-name'>{item.name}</Text>
{/* <Text className='item-desc'>{item.description}</Text> */}
</View>
<View className='price-row'>
<Text className='price'>¥{item.price}</Text>
<View className='quantity-control'>
<View className='btn-qty' onClick={() => handleUpdateQuantity(item.id, -1)}></View>
<Text className='qty-num'>{item.quantity}</Text>
<View className='btn-qty' onClick={() => handleUpdateQuantity(item.id, 1)}>+</View>
</View>
</View>
</View>
<View className='btn-delete' onClick={() => handleRemove(item.id)}>×</View>
</View>
))}
</ScrollView>
)}
{cartItems.length > 0 && (
<View className='bottom-bar'>
<View className='left-section'>
<View className='select-all-btn' onClick={handleToggleAll}>
<View className={`checkbox ${isAllSelected ? 'checked' : ''}`}>
{isAllSelected && <Text></Text>}
</View>
<Text className='label'></Text>
</View>
<View className='total-info'>
<Text className='label'>:</Text>
<Text className='price'>¥{totalPrice}</Text>
</View>
</View>
<Button
className={`btn-checkout ${selectedCount === 0 ? 'disabled' : ''}`}
onClick={handleCheckout}
>
({selectedCount})
</Button>
</View>
)}
</View>
)
}

View File

@@ -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;

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-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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { useState } from 'react'
import { useState, useMemo } from 'react'
import { getConfigDetail, createOrder } from '../../api'
import { getSelectedItems, removeItem } from '../../utils/cart'
import './checkout.scss'
export default function Checkout() {
const router = useRouter()
const { id, quantity } = router.params
const [product, setProduct] = useState<any>(null)
const params = router.params
const [items, setItems] = useState<any[]>([])
const [address, setAddress] = useState<any>(null)
const [contact, setContact] = useState({ name: '', phone: '' })
const [deliveryType, setDeliveryType] = useState<'delivery' | 'pickup'>('delivery')
const [userAddress, setUserAddress] = useState<any>(null)
const [loading, setLoading] = useState(true)
const PICKUP_ADDRESS = {
userName: '云南量迹科技有限公司',
telNumber: '18585164448',
provinceName: '云南省',
cityName: '昆明市',
countyName: '西山区',
detailInfo: '永昌街道办事处云纺国际商厦 B 座 1406 号'
}
useLoad(async () => {
if (id) {
const res = await getConfigDetail(Number(id))
setProduct(res)
if (params.from === 'cart') {
const cartItems = getSelectedItems()
if (cartItems.length === 0) {
Taro.navigateBack()
return
}
setItems(cartItems)
setLoading(false)
} else if (params.id) {
try {
const res = await getConfigDetail(params.id)
setItems([{
id: res.id,
name: res.name,
price: res.price,
image: res.static_image_url || res.detail_image_url,
quantity: Number(params.quantity) || 1,
description: res.description
}])
} catch (err) {
console.error(err)
Taro.showToast({ title: '商品加载失败', icon: 'none' })
} finally {
setLoading(false)
}
}
})
const chooseAddress = async () => {
if (deliveryType === 'pickup') return
try {
const res = await Taro.chooseAddress()
setAddress(res)
setContact({ name: res.userName, phone: res.telNumber })
} catch (e) {
Taro.showToast({ title: '需要授权获取地址', icon: 'none' })
const res = await Taro.chooseAddress()
setAddress(res)
setUserAddress(res)
} catch (err) {
console.error(err)
// User cancelled or auth denied
}
}
const handleTypeChange = (type: 'delivery' | 'pickup') => {
if (type === deliveryType) return
setDeliveryType(type)
if (type === 'pickup') {
setAddress(PICKUP_ADDRESS)
} else {
setAddress(userAddress)
}
}
const totalPrice = useMemo(() => {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}, [items])
const submitOrder = async () => {
if (!address) {
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
return
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
return
}
Taro.showLoading({ title: '提交中...' })
try {
Taro.showLoading({ title: '正在下单...' })
const orderPromises = items.map(item => {
const orderData = {
goodid: product.id,
quantity: Number(quantity || 1),
customer_name: contact.name,
phone_number: contact.phone,
shipping_address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}`,
// ref_code: Taro.getStorageSync('ref_code')
}
const res = await createOrder(orderData)
Taro.hideLoading()
if (res.order_id) {
Taro.redirectTo({ url: `/pages/order/payment?id=${res.order_id}` })
goodid: item.id,
quantity: item.quantity,
customer_name: address.userName,
phone_number: address.telNumber,
shipping_address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}`
}
return createOrder(orderData)
})
const results = await Promise.all(orderPromises)
// If from cart, remove bought items
if (params.from === 'cart') {
items.forEach(item => removeItem(item.id))
}
Taro.hideLoading()
if (results.length === 1) {
// Single order, go to payment
const orderId = results[0].order_id
Taro.redirectTo({
url: `/pages/order/payment?id=${orderId}`
})
} else {
// Multiple orders
Taro.showModal({
title: '下单成功',
content: `成功创建 ${results.length} 个订单,请前往订单列表支付`,
showCancel: false,
confirmText: '去支付',
success: () => {
Taro.redirectTo({ url: '/pages/order/list' })
}
})
}
} catch (err) {
Taro.hideLoading()
console.error(err)
Taro.hideLoading()
console.error(err)
Taro.showToast({ title: '下单失败', icon: 'none' })
}
}
if (!product) return <View>Loading...</View>
if (loading) return <View className='page-container'><View className='section'><Text>Loading...</Text></View></View>
return (
<View className='page-container'>
<View className='page-container'>
<ScrollView scrollY style={{height: 'calc(100vh - 120px)'}}>
{/* Delivery Type Section */}
<View className='section delivery-type-section'>
<View
className={`type-item ${deliveryType === 'delivery' ? 'active' : ''}`}
onClick={() => handleTypeChange('delivery')}
>
</View>
<View
className={`type-item ${deliveryType === 'pickup' ? 'active' : ''}`}
onClick={() => handleTypeChange('pickup')}
>
</View>
</View>
{/* Address Section */}
<View className='section address-section' onClick={chooseAddress}>
{address ? (
<View>
<View className='row'>
<Text className='name'>{contact.name}</Text>
<Text className='phone'>{contact.phone}</Text>
<View className='address-info'>
<View className='user-info'>
<Text>{address.userName}</Text>
<Text className='phone'>{address.telNumber}</Text>
</View>
<View className='address-text'>
{address.provinceName}{address.cityName}{address.countyName}{address.detailInfo}
</View>
<Text className='addr'>{address.provinceName}{address.cityName}{address.countyName}{address.detailInfo}</Text>
</View>
) : (
<View className='placeholder-container'>
<Text className='placeholder'>+ </Text>
<View className='address-info'>
<Text className='placeholder'>+ </Text>
</View>
)}
{deliveryType === 'delivery' && <Text className='arrow'></Text>}
</View>
{/* Products Section */}
<View className='section product-section'>
<Text className='p-name'>{product.name}</Text>
<View className='row'>
<Text className='p-price'>¥{product.price}</Text>
<Text className='p-qty'>x {quantity}</Text>
</View>
<View className='divider' />
<View className='row total-row'>
<Text></Text>
<Text className='total-price'>¥{(product.price * (Number(quantity) || 1)).toFixed(2)}</Text>
</View>
<Text className='section-title'></Text>
{items.map((item, idx) => (
<View key={idx} className='product-item'>
<Image src={item.image} className='p-img' mode='aspectFill' />
<View className='p-info'>
<Text className='p-name'>{item.name}</Text>
<Text className='p-desc'>{item.description}</Text>
<View className='p-meta'>
<Text className='p-price'>¥{item.price}</Text>
<Text className='p-qty'>x{item.quantity}</Text>
</View>
</View>
</View>
))}
</View>
<View className='bottom-bar safe-area-bottom'>
<Button className='btn-submit' onClick={submitOrder}></Button>
{/* Summary Section */}
<View className='section summary-section'>
<View className='row'>
<Text></Text>
<Text>¥{totalPrice}</Text>
</View>
<View className='row'>
<Text></Text>
<Text>¥0</Text>
</View>
<View className='row total'>
<Text></Text>
<Text className='price'>¥{totalPrice}</Text>
</View>
</View>
</View>
</ScrollView>
{/* Bottom Bar */}
<View className='bottom-bar'>
<Text className='total-label'>{items.length}</Text>
<Text className='total-price'>¥{totalPrice}</Text>
<Button className='btn-submit' onClick={submitOrder}></Button>
</View>
</View>
)
}

View File

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

View File

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

View File

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

View File

@@ -13,33 +13,99 @@ export default function UserIndex() {
const goOrders = () => Taro.navigateTo({ url: '/pages/order/list' })
const goDistributor = () => Taro.navigateTo({ url: '/subpackages/distributor/index' })
const goInvite = () => Taro.navigateTo({ url: '/subpackages/distributor/invite' })
const goWithdraw = () => Taro.navigateTo({ url: '/subpackages/distributor/withdraw' })
const handleAddress = async () => {
try { await Taro.chooseAddress() } catch(e) {}
}
const login = () => {
// Trigger login again if needed
Taro.reLaunch({ url: '/pages/index/index' })
}
const serviceGroups = [
{
title: '基础服务',
items: [
{ title: '我的订单', icon: '📦', action: goOrders },
{ title: '地址管理', icon: '📍', action: handleAddress },
{ title: '新增地址', icon: '📝', action: handleAddress },
]
},
{
title: '分销中心',
items: [
{ title: '分销首页', icon: '⚡', action: goDistributor },
{ title: '推广邀请', icon: '🤝', action: goInvite },
{ title: '佣金提现', icon: '💰', action: goWithdraw },
]
},
{
title: '其他',
items: [
{ title: '联系客服', icon: '🎧', isContact: true }
]
}
]
const stats = [
{ label: '余额', value: '0.00' },
{ label: '积分', value: '0' },
{ label: '优惠券', value: '0' }
]
return (
<View className='page-container'>
<View className='header'>
<Image src={userInfo?.avatar_url || 'https://via.placeholder.com/100'} className='avatar' />
<Text className='nickname'>{userInfo?.nickname || '未登录'}</Text>
{!userInfo && <Button size='mini' onClick={login}></Button>}
{/* Profile Card */}
<View className='profile-card'>
<View className='avatar-container'>
<Image src={userInfo?.avatar_url || 'https://via.placeholder.com/150/00b96b/FFFFFF?text=USER'} className='avatar' />
{userInfo && <View className='online-dot' />}
</View>
<View className='info-col'>
<Text className='nickname'>{userInfo?.nickname || '未登录用户'}</Text>
<Text className='uid'>ID: {userInfo ? '888888' : '----'}</Text>
{!userInfo && (
<Button className='btn-login' onClick={login}> / </Button>
)}
</View>
<View className='card-bg-effect' />
</View>
<View className='menu'>
<View className='item' onClick={goOrders}>
<Text></Text>
<Text className='arrow'>></Text>
</View>
<View className='item' onClick={goDistributor}>
<Text></Text>
<Text className='arrow'>></Text>
</View>
<View className='item'>
<Text></Text>
<Button openType='contact' className='btn-contact' />
<Text className='arrow'>></Text>
</View>
{/* Stats Row */}
<View className='stats-row'>
{stats.map((item, idx) => (
<View key={idx} className='stat-item'>
<Text className='stat-val'>{item.value}</Text>
<Text className='stat-lbl'>{item.label}</Text>
</View>
))}
</View>
{/* Service Groups */}
<View className='service-container'>
{serviceGroups.map((group, gIdx) => (
<View key={gIdx} className='service-group'>
<Text className='group-title'>{group.title}</Text>
<View className='grid-layout'>
{group.items.map((item, idx) => (
<View key={idx} className='grid-item' onClick={item.action}>
<View className='icon-box'>
<Text className='icon'>{item.icon}</Text>
</View>
<Text className='item-title'>{item.title}</Text>
{item.isContact && <Button openType='contact' className='contact-overlay' />}
</View>
))}
</View>
</View>
))}
</View>
<View className='version-info'>
<Text>Quant Speed Market v1.0.0</Text>
<Text>Powered by Taro & React</Text>
</View>
</View>
)

View File

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

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