This commit is contained in:
jeremygan2021
2026-02-11 01:31:29 +08:00
parent 2d090cd0f4
commit c3b4373c94
18 changed files with 894 additions and 263 deletions

30
backend/check_urls.py Normal file
View File

@@ -0,0 +1,30 @@
import os
import django
from django.urls import reverse
from django.conf import settings
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
links = [
"admin:shop_wechatuser_changelist",
"admin:shop_salesperson_changelist",
"admin:shop_distributor_changelist",
"admin:shop_esp32config_changelist",
"admin:shop_service_changelist",
"admin:shop_arservice_changelist",
"admin:shop_order_changelist",
"admin:shop_serviceorder_changelist",
"admin:shop_withdrawal_changelist",
"admin:shop_commissionlog_changelist",
"admin:shop_wechatpayconfig_changelist",
"admin:auth_user_changelist",
]
print("Checking URL patterns...")
for link in links:
try:
url = reverse(link)
print(f"[OK] {link} -> {url}")
except Exception as e:
print(f"[ERROR] {link}: {e}")

View File

@@ -170,26 +170,118 @@ SPECTACULAR_SETTINGS = {
'REDOC_DIST': 'SIDECAR',
}
from django.urls import reverse_lazy
# django-unfold配置
UNFOLD = {
"SITE_TITLE": "科技公司产品管理",
"SITE_HEADER": "科技公司产品购买系统",
"SITE_TITLE": "量迹AI后台",
"SITE_HEADER": "量迹AI科技硬件/服务商场后台",
"SITE_URL": "/",
"COLORS": {
"primary": {
"50": "rgb(240 249 255)",
"100": "rgb(224 242 254)",
"200": "rgb(186 230 253)",
"300": "rgb(125 211 252)",
"400": "rgb(56 189 248)",
"500": "rgb(14 165 233)",
"600": "rgb(2 132 199)",
"700": "rgb(3 105 161)",
"800": "rgb(7 89 133)",
"900": "rgb(12 74 110)",
"950": "rgb(8 47 73)",
"50": "rgb(236 254 255)",
"100": "rgb(207 250 254)",
"200": "rgb(165 243 252)",
"300": "rgb(103 232 249)",
"400": "rgb(34 211 238)",
"500": "rgb(6 182 212)",
"600": "rgb(8 145 178)",
"700": "rgb(14 116 144)",
"800": "rgb(21 94 117)",
"900": "rgb(22 78 99)",
"950": "rgb(8 51 68)",
},
},
"SIDEBAR": {
"show_search": True,
"show_all_applications": False,
"navigation": [
{
"title": "用户管理",
"separator": True,
"items": [
{
"title": "微信用户",
"icon": "people",
"link": reverse_lazy("admin:shop_wechatuser_changelist"),
},
{
"title": "分销员管理",
"icon": "supervisor_account",
"link": reverse_lazy("admin:shop_salesperson_changelist"),
},
{
"title": "小程序分销员",
"icon": "groups",
"link": reverse_lazy("admin:shop_distributor_changelist"),
},
],
},
{
"title": "商品管理",
"separator": True,
"items": [
{
"title": "硬件配置 (小智参数)",
"icon": "hardware",
"link": reverse_lazy("admin:shop_esp32config_changelist"),
},
{
"title": "AI服务",
"icon": "smart_toy",
"link": reverse_lazy("admin:shop_service_changelist"),
},
{
"title": "AR体验",
"icon": "view_in_ar",
"link": reverse_lazy("admin:shop_arservice_changelist"),
},
],
},
{
"title": "交易管理",
"separator": True,
"items": [
{
"title": "订单列表",
"icon": "shopping_cart",
"link": reverse_lazy("admin:shop_order_changelist"),
},
{
"title": "服务订单",
"icon": "assignment",
"link": reverse_lazy("admin:shop_serviceorder_changelist"),
},
{
"title": "提现管理",
"icon": "account_balance_wallet",
"link": reverse_lazy("admin:shop_withdrawal_changelist"),
},
{
"title": "佣金记录",
"icon": "monetization_on",
"link": reverse_lazy("admin:shop_commissionlog_changelist"),
},
],
},
{
"title": "系统配置",
"separator": True,
"items": [
{
"title": "微信支付配置",
"icon": "payment",
"link": reverse_lazy("admin:shop_wechatpayconfig_changelist"),
},
{
"title": "用户认证",
"icon": "security",
"link": reverse_lazy("admin:auth_user_changelist"),
},
],
},
],
},
}
# 重新启用自动补齐斜杠,方便 Admin 使用

View File

@@ -4,13 +4,13 @@ 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
from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder
import qrcode
from io import BytesIO
import base64
# 自定义后台标题
admin.site.site_header = "量迹AI硬件销售管理后台"
admin.site.site_header = "量迹AI科技硬件/服务商场后台"
admin.site.site_title = "量迹AI后台"
admin.site.index_title = "欢迎使用量迹AI管理系统"
@@ -122,6 +122,25 @@ class ServiceAdmin(ModelAdmin):
}),
)
@admin.register(ServiceOrder)
class ServiceOrderAdmin(ModelAdmin):
list_display = ('id', 'customer_name', 'service', 'total_price', 'status', 'salesperson', 'created_at')
list_filter = ('status', 'service', 'salesperson', 'created_at')
search_fields = ('id', 'customer_name', 'phone_number', 'email')
readonly_fields = ('total_price', 'created_at', 'updated_at')
fieldsets = (
('订单信息', {
'fields': ('service', 'status', 'total_price', 'created_at')
}),
('客户信息', {
'fields': ('customer_name', 'company_name', 'phone_number', 'email', 'requirements')
}),
('销售归属', {
'fields': ('salesperson',)
}),
)
@admin.register(ARService)
class ARServiceAdmin(ModelAdmin):
list_display = ('title', 'created_at')
@@ -251,3 +270,84 @@ class OrderAdmin(ModelAdmin):
'fields': ('wechat_trade_no',)
}),
)
@admin.register(WeChatUser)
class WeChatUserAdmin(ModelAdmin):
list_display = ('nickname', 'avatar_display', 'gender_display', 'province', 'city', 'created_at')
search_fields = ('nickname', 'openid')
list_filter = ('gender', 'province', 'city', 'created_at')
readonly_fields = ('openid', 'unionid', 'session_key', 'created_at', 'updated_at')
def avatar_display(self, obj):
if obj.avatar_url:
return format_html('<img src="{}" width="50" height="50" style="border-radius: 50%;" />', obj.avatar_url)
return "暂无"
avatar_display.short_description = "头像"
def gender_display(self, obj):
choices = {0: '未知', 1: '', 2: ''}
return choices.get(obj.gender, '未知')
gender_display.short_description = "性别"
fieldsets = (
('基本信息', {
'fields': ('user', 'nickname', 'avatar_url', 'gender')
}),
('位置信息', {
'fields': ('country', 'province', 'city')
}),
('认证信息', {
'fields': ('openid', 'unionid', 'session_key'),
'classes': ('collapse',)
}),
('时间信息', {
'fields': ('created_at', 'updated_at')
}),
)
@admin.register(Distributor)
class DistributorAdmin(ModelAdmin):
list_display = ('get_nickname', 'level', 'status', 'total_earnings', 'withdrawable_balance', 'invite_code', 'created_at')
search_fields = ('user__nickname', 'invite_code')
list_filter = ('status', 'level', 'created_at')
readonly_fields = ('total_earnings', 'withdrawable_balance', 'qr_code_url', 'created_at', 'updated_at')
autocomplete_fields = ['user', 'parent']
def get_nickname(self, obj):
return obj.user.nickname
get_nickname.short_description = "微信昵称"
get_nickname.admin_order_field = 'user__nickname'
fieldsets = (
('分销员信息', {
'fields': ('user', 'parent', 'level', 'status')
}),
('收益概览', {
'fields': ('commission_rate', 'total_earnings', 'withdrawable_balance')
}),
('推广信息', {
'fields': ('invite_code', 'qr_code_url')
}),
('时间信息', {
'fields': ('created_at', 'updated_at')
}),
)
@admin.register(Withdrawal)
class WithdrawalAdmin(ModelAdmin):
list_display = ('get_distributor', 'amount', 'status', 'created_at')
list_filter = ('status', 'created_at')
search_fields = ('distributor__user__nickname',)
def get_distributor(self, obj):
return obj.distributor.user.nickname
get_distributor.short_description = "分销员"
fieldsets = (
('提现详情', {
'fields': ('distributor', 'amount', 'status', 'remark')
}),
('时间信息', {
'fields': ('created_at', 'updated_at')
}),
)

View File

@@ -0,0 +1,6 @@
<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"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -0,0 +1,10 @@
.particle-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
background: #000;
}

View File

@@ -0,0 +1,175 @@
import { Canvas, View } from '@tarojs/components'
import Taro, { useReady, useUnload } from '@tarojs/taro'
import { useRef } from 'react'
import './index.scss'
export default function ParticleBackground() {
const canvasRef = useRef<any>(null)
const animationRef = useRef<any>(null)
useReady(() => {
const query = Taro.createSelectorQuery()
query.select('#particle-canvas')
.fields({ node: true, size: true })
.exec((res) => {
if (!res[0]) return
const canvas = res[0].node
const ctx = canvas.getContext('2d')
const dpr = Taro.getSystemInfoSync().pixelRatio
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
const width = res[0].width
const height = res[0].height
// Init particles
const particles: any[] = []
const particleCount = 40 // Reduced for mobile performance
const meteors: any[] = []
const meteorCount = 4
class Particle {
x: number
y: number
vx: number
vy: number
size: number
color: string
constructor() {
this.x = Math.random() * width
this.y = Math.random() * height
this.vx = (Math.random() - 0.5) * 0.5
this.vy = (Math.random() - 0.5) * 0.5
this.size = Math.random() * 2
this.color = Math.random() > 0.5 ? 'rgba(0, 185, 107, ' : 'rgba(0, 240, 255, '
}
update() {
this.x += this.vx
this.y += this.vy
if (this.x < 0 || this.x > width) this.vx *= -1
if (this.y < 0 || this.y > height) this.vy *= -1
}
draw() {
ctx.beginPath()
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
ctx.fillStyle = this.color + (0.5 + Math.random() * 0.5) + ')'
ctx.fill()
}
}
class Meteor {
x: number
y: number
vx: number
vy: number
len: number
color: string
opacity: number
maxOpacity: number
wait: number
constructor() {
this.x = 0
this.y = 0
this.vx = 0
this.vy = 0
this.len = 0
this.color = ''
this.opacity = 0
this.maxOpacity = 0
this.wait = 0
this.reset()
}
reset() {
this.x = Math.random() * width * 1.5
this.y = Math.random() * -height
this.vx = -(Math.random() * 3 + 3)
this.vy = Math.random() * 3 + 3
this.len = Math.random() * 100 + 100
this.color = Math.random() > 0.5 ? 'rgba(0, 185, 107, ' : 'rgba(0, 240, 255, '
this.opacity = 0
this.maxOpacity = Math.random() * 0.5 + 0.2
this.wait = Math.random() * 200
}
update() {
if (this.wait > 0) {
this.wait--
return
}
this.x += this.vx
this.y += this.vy
if (this.opacity < this.maxOpacity) this.opacity += 0.02
if (this.x < -this.len || this.y > height + this.len) this.reset()
}
draw() {
if (this.wait > 0) return
const tailX = this.x - this.vx * (this.len / 15)
const tailY = this.y - this.vy * (this.len / 15)
const gradient = ctx.createLinearGradient(this.x, this.y, tailX, tailY)
gradient.addColorStop(0, this.color + this.opacity + ')')
gradient.addColorStop(1, this.color + '0)')
ctx.save()
ctx.beginPath()
ctx.strokeStyle = gradient
ctx.lineWidth = 2
ctx.lineCap = 'round'
ctx.moveTo(this.x, this.y)
ctx.lineTo(tailX, tailY)
ctx.stroke()
ctx.restore()
}
}
for (let i = 0; i < particleCount; i++) particles.push(new Particle())
for (let i = 0; i < meteorCount; i++) meteors.push(new Meteor())
const animate = () => {
ctx.clearRect(0, 0, width, height)
// Draw Meteors
meteors.forEach(m => {
m.update()
m.draw()
})
// Draw Lines
ctx.lineWidth = 0.5
for (let i = 0; i < particleCount; i++) {
for (let j = i; j < particleCount; j++) {
const dx = particles[i].x - particles[j].x
const dy = particles[i].y - particles[j].y
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist < 80) { // Reduced distance for mobile
ctx.beginPath()
ctx.strokeStyle = `rgba(100, 255, 218, ${1 - dist / 80})`
ctx.moveTo(particles[i].x, particles[i].y)
ctx.lineTo(particles[j].x, particles[j].y)
ctx.stroke()
}
}
}
// Draw Particles
particles.forEach(p => {
p.update()
p.draw()
})
canvas.requestAnimationFrame(animate)
}
animate()
})
})
return (
<Canvas
type='2d'
id='particle-canvas'
className='particle-canvas'
/>
)
}

View File

@@ -1,138 +1,223 @@
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #000;
color: #fff;
position: relative;
}
.loading-screen, .error-screen {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #666;
}
.content {
flex: 1;
overflow-y: auto;
height: 100vh;
background: #000;
}
.detail-img {
width: 100%;
display: block;
.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);
}
.info-section {
padding: 20px;
}
.title {
font-size: 24px;
font-weight: bold;
color: #00f0ff;
display: block;
margin-bottom: 10px;
}
.price {
font-size: 28px;
color: #00b96b;
font-weight: bold;
display: block;
margin-bottom: 20px;
}
.specs {
display: flex;
background: rgba(255,255,255,0.05);
border-radius: 8px;
padding: 15px;
.hero-section {
position: relative;
margin-bottom: 20px;
.spec-item {
flex: 1;
text-align: center;
border-right: 1px solid rgba(255,255,255,0.1);
&:last-child {
border-right: none;
}
.label {
font-size: 12px;
color: #888;
.image-container {
width: 100%;
min-height: 600px;
background: radial-gradient(circle at center, #1a1a1a, #000);
position: relative;
display: flex;
align-items: center;
justify-content: center;
.hero-img {
width: 100%;
display: block;
margin-bottom: 5px;
}
.placeholder-box {
.icon-bolt { font-size: 100px; }
}
.value {
font-size: 14px;
.hero-overlay {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 60%;
background: linear-gradient(to top, #000 10%, transparent);
}
}
.hero-content {
padding: 0 30px;
margin-top: -100px; // Pull up over image
position: relative;
z-index: 2;
.hero-title {
font-size: 48px;
font-weight: 900;
color: #fff;
font-weight: bold;
display: block;
margin-bottom: 15px;
text-shadow: 0 0 20px rgba(0,0,0,0.8);
}
.hero-desc {
font-size: 28px;
color: #ccc;
line-height: 1.5;
display: block;
margin-bottom: 25px;
text-shadow: 0 0 10px rgba(0,0,0,0.8);
}
.tags-row {
display: flex;
flex-wrap: wrap;
gap: 15px;
.tag {
padding: 8px 20px;
border-radius: 30px;
font-size: 24px;
backdrop-filter: blur(10px);
&.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); }
}
}
}
}
.desc {
margin-bottom: 20px;
.stats-card {
margin: 0 30px 40px;
border-radius: 24px;
padding: 30px;
display: flex;
align-items: center;
justify-content: space-around;
.section-title {
font-size: 16px;
color: #fff;
margin-bottom: 10px;
display: block;
border-left: 3px solid #00f0ff;
padding-left: 10px;
.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; }
}
.divider { width: 1px; height: 60px; background: rgba(255,255,255,0.1); }
}
.text {
font-size: 14px;
color: #ccc;
line-height: 1.6;
.features-section {
padding: 0 30px;
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 40px;
.feature-card {
padding: 30px;
border-radius: 20px;
display: flex;
align-items: flex-start;
.feature-icon-box {
width: 80px;
height: 80px;
margin-right: 25px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,255,255,0.05);
border-radius: 16px;
.f-icon { font-size: 40px; color: #00f0ff; }
.f-icon-img { width: 50px; height: 50px; }
}
.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; }
}
}
}
.feature-item {
margin-bottom: 15px;
.f-title {
font-size: 15px;
color: #00f0ff;
margin-bottom: 5px;
display: block;
}
.f-desc {
font-size: 13px;
color: #bbb;
}
.detail-image-section {
width: 100%;
margin-bottom: 40px;
.long-detail-img { width: 100%; display: block; }
}
.footer-spacer { height: 160px; }
.bottom-bar {
background: #111;
padding: 10px 20px;
border-top: 1px solid rgba(255,255,255,0.1);
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20px 30px;
z-index: 100;
border-top-left-radius: 30px;
border-top-right-radius: 30px;
background: rgba(20, 20, 20, 0.95); // Darker for contrast
.btn-container {
.action-row {
display: flex;
align-items: center;
gap: 20px;
height: 100px;
.cart-icon-btn {
display: flex;
gap: 15px;
}
.btn-cart, .btn-buy {
flex: 1;
border: none;
color: #fff;
font-size: 16px;
height: 44px;
line-height: 44px;
border-radius: 22px;
margin: 0;
}
.btn-cart {
background: #333;
}
.btn-buy {
background: #00b96b;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 20px;
.icon { font-size: 40px; margin-bottom: 5px; }
.label { font-size: 20px; color: #888; }
}
.btn-add-cart, .btn-buy-now {
flex: 1;
height: 80px;
line-height: 80px;
border-radius: 40px;
font-size: 28px;
font-weight: bold;
border: none;
margin: 0;
&::after { border: none; }
}
.btn-add-cart {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.btn-buy-now {
background: linear-gradient(90deg, #00b96b, #00f0ff);
color: #000;
box-shadow: 0 5px 20px rgba(0, 185, 107, 0.3);
}
}
}
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
padding-bottom: calc(20px + constant(safe-area-inset-bottom));
padding-bottom: calc(20px + env(safe-area-inset-bottom));
}

View File

@@ -8,6 +8,7 @@ export default function Detail() {
const router = useRouter()
const { id } = router.params
const [product, setProduct] = useState<any>(null)
const [loading, setLoading] = useState(true)
useLoad(() => {
if (id) fetchDetail(id)
@@ -19,6 +20,9 @@ export default function Detail() {
setProduct(res)
} catch (err) {
console.error(err)
Taro.showToast({ title: '加载失败', icon: 'none' })
} finally {
setLoading(false)
}
}
@@ -29,50 +33,91 @@ export default function Detail() {
})
}
if (!product) return <View className='loading'>Loading...</View>
if (loading) return <View className='loading-screen'><Text>Loading...</Text></View>
if (!product) return <View className='error-screen'><Text>Product Not Found</Text></View>
return (
<View className='page-container'>
<ScrollView scrollY className='content'>
<Image src={product.detail_image_url || product.static_image_url || 'https://via.placeholder.com/400x400'} mode='widthFix' className='detail-img' />
<View className='info-section'>
<Text className='title'>{product.name}</Text>
<Text className='price'>¥{product.price}</Text>
<View className='specs'>
<View className='spec-item'>
<Text className='label'></Text>
<Text className='value'>{product.chip_type}</Text>
</View>
<View className='spec-item'>
<Text className='label'>Flash</Text>
<Text className='value'>{product.flash_size}MB</Text>
</View>
<View className='spec-item'>
<Text className='label'>RAM</Text>
<Text className='value'>{product.ram_size}MB</Text>
</View>
</View>
<View className='desc'>
<Text className='section-title'></Text>
<Text className='text'>{product.description}</Text>
{/* 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' />
) : (
<View className='placeholder-box'>
<Text className='icon-bolt'></Text>
</View>
)}
<View className='hero-overlay' />
</View>
{product.features && product.features.map((f, idx) => (
<View key={idx} className='feature-item'>
<Text className='f-title'> {f.title}</Text>
<Text className='f-desc'>{f.description}</Text>
</View>
))}
<View className='hero-content'>
<Text className='hero-title'>{product.name}</Text>
<Text className='hero-desc'>{product.description}</Text>
<View className='tags-row'>
<View className='tag cyan'><Text>{product.chip_type}</Text></View>
{product.has_camera && <View className='tag blue'><Text></Text></View>}
{product.has_microphone && <View className='tag purple'><Text></Text></View>}
</View>
</View>
</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>
<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>
</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>}
</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>
{/* Detail Image */}
{product.detail_image_url && (
<View className='detail-image-section'>
<Image src={product.detail_image_url} mode='widthFix' className='long-detail-img' />
</View>
)}
<View className='footer-spacer' />
</ScrollView>
<View className='bottom-bar safe-area-bottom'>
<View className='btn-container'>
<Button className='btn-cart' onClick={() => Taro.showToast({title: '加入购物车', icon:'none'})}></Button>
<Button className='btn-buy' onClick={buyNow}></Button>
{/* Bottom Bar */}
<View className='bottom-bar glass-panel safe-area-bottom'>
<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>
</View>
</View>
</View>

View File

@@ -1,50 +1,79 @@
.page-container {
min-height: 100vh;
height: 100vh;
background-color: #000;
color: #fff;
padding: 20px;
overflow: hidden;
position: relative;
}
.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;
margin-bottom: 40px;
padding-top: 40px;
padding: 60px 20px 40px;
position: relative;
.logo-placeholder {
font-size: 24px;
font-weight: bold;
color: #00f0ff;
margin-bottom: 20px;
letter-spacing: 2px;
.logo-box {
margin-bottom: 30px;
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));
}
.logo-text {
font-size: 40px;
font-weight: 900;
color: #fff;
letter-spacing: 6px;
text-shadow: 0 0 20px rgba(0, 240, 255, 0.6);
}
}
.title-container {
margin-bottom: 20px;
margin-bottom: 25px;
display: flex;
justify-content: center;
align-items: center;
height: 60px;
}
.title-text {
font-size: 32px;
font-size: 36px;
font-weight: bold;
color: #00f0ff;
text-shadow: 0 0 10px rgba(0, 240, 255, 0.5);
text-shadow: 0 0 15px rgba(0, 240, 255, 0.5);
}
.cursor {
font-size: 32px;
font-size: 36px;
color: #fff;
margin-left: 5px;
margin-left: 8px;
animation: blink 1s infinite;
}
.subtitle {
color: #aaa;
font-size: 14px;
font-size: 26px;
line-height: 1.6;
display: block;
padding: 0 20px;
padding: 0 40px;
font-weight: 300;
}
}
@@ -53,108 +82,158 @@
50% { opacity: 0; }
}
.product-scroll {
width: 100%;
white-space: nowrap;
.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;}
}
.product-list {
.product-grid {
padding: 0 30px;
display: flex;
padding-bottom: 20px;
flex-direction: column;
gap: 40px;
}
.card {
display: inline-block;
width: 280px;
background: linear-gradient(135deg, rgba(31,31,31,0.9), rgba(42,42,42,0.9));
border-radius: 12px;
margin-right: 20px;
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 24px;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
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;
&:active {
transform: scale(0.98);
border-color: #00b96b;
box-shadow: 0 0 30px rgba(0, 185, 107, 0.2);
}
&-cover {
height: 180px;
background: #222;
height: 360px;
background: #111;
position: relative;
overflow: hidden;
.card-img {
width: 100%;
height: 100%;
transition: transform 0.5s ease;
}
.placeholder-img {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at center, #222, #111);
.icon-rocket { font-size: 100px; }
}
.card-overlay {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50%;
background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
}
}
&-body {
padding: 16px;
padding: 30px;
}
&-title {
font-size: 18px;
font-weight: bold;
color: #00f0ff;
display: block;
margin-bottom: 8px;
white-space: normal;
&-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
.card-title {
font-size: 36px;
font-weight: bold;
color: #fff;
flex: 1;
margin-right: 20px;
line-height: 1.3;
text-shadow: 0 0 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);
}
}
&-desc {
font-size: 12px;
color: #bbb;
display: block;
margin-bottom: 12px;
height: 36px;
overflow: hidden;
white-space: normal;
font-size: 26px;
color: #ccc;
line-height: 1.5;
margin-bottom: 25px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.tags {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 30px;
.tag {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
padding: 8px 18px;
border-radius: 12px;
font-size: 22px;
font-weight: 500;
&.cyan {
color: cyan;
background: rgba(0,255,255,0.1);
border: 1px solid cyan;
color: #00f0ff;
background: rgba(0, 240, 255, 0.1);
border: 1px solid rgba(0, 240, 255, 0.3);
}
&.blue {
color: blue;
background: rgba(0,0,255,0.1);
border: 1px solid blue;
color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.3);
}
&.purple {
color: #a855f7;
background: rgba(168, 85, 247, 0.1);
border: 1px solid rgba(168, 85, 247, 0.3);
}
}
}
&-footer {
display: flex;
justify-content: space-between;
align-items: center;
.price {
font-size: 20px;
color: #00b96b;
.btn-buy {
background: linear-gradient(90deg, #00b96b, #00f0ff);
color: #000;
font-weight: bold;
}
.btn-arrow {
width: 30px;
height: 30px;
border-radius: 50%;
background: #00b96b;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
font-size: 30px;
border-radius: 50px;
border: none;
height: 80px;
line-height: 80px;
box-shadow: 0 5px 20px rgba(0, 185, 107, 0.3);
&:active {
opacity: 0.9;
}
}
}
}
.footer-spacer {
height: 100px;
}

View File

@@ -2,6 +2,7 @@ import { View, Text, Image, ScrollView, Button } from '@tarojs/components'
import Taro, { useLoad } from '@tarojs/taro'
import { useState, useEffect } from 'react'
import { getConfigs } from '../../api'
import ParticleBackground from '../../components/ParticleBackground'
import './index.scss'
export default function Index() {
@@ -30,12 +31,10 @@ export default function Index() {
setError('')
try {
const res: any = await getConfigs()
console.log('Configs fetched:', res)
// Adapt to different API response structures
const list = Array.isArray(res) ? res : (res.results || res.data || [])
setProducts(list)
} catch (err: any) {
console.error('Fetch error:', err)
console.error(err)
setError(err.errMsg || '加载失败,请检查网络')
} finally {
setLoading(false)
@@ -48,34 +47,34 @@ export default function Index() {
return (
<View className='page-container'>
<View className='header'>
<View className='logo-box'>
<Text className='logo-text'>QUANT SPEED</Text>
<ParticleBackground />
<ScrollView scrollY className='content-scroll'>
<View className='scroll-inner'>
<View className='header'>
<View className='logo-box'>
<Image src='../../assets/logo.svg' className='logo-img' mode='widthFix' />
<Text className='logo-text'>QUANT SPEED</Text>
</View>
<View className='title-container'>
<Text className='title-text'>{typedText}</Text>
<Text className='cursor'>|</Text>
</View>
<Text className='subtitle'> AI </Text>
</View>
<View className='title-container'>
<Text className='title-text'>{typedText}</Text>
<Text className='cursor'>|</Text>
</View>
<Text className='subtitle'> AI </Text>
</View>
{loading ? (
<View className='status-box'>
<Text className='loading-text'>...</Text>
</View>
) : error ? (
<View className='status-box'>
<Text className='error-text'>{error}</Text>
<Button className='btn-retry' onClick={fetchProducts}></Button>
</View>
) : products.length === 0 ? (
<View className='status-box'>
<Text className='empty-text'></Text>
</View>
) : (
<ScrollView scrollX className='product-scroll' enableFlex>
<View className='product-list'>
{loading ? (
<View className='status-box'>
<Text className='loading-text'>...</Text>
</View>
) : error ? (
<View className='status-box'>
<Text className='error-text'>{error}</Text>
<Button className='btn-retry' onClick={fetchProducts}></Button>
</View>
) : (
<View className='product-grid'>
{products.map((item) => (
<View key={item.id} className='card' onClick={() => goToDetail(item.id)}>
<View className='card-cover'>
@@ -86,25 +85,35 @@ export default function Index() {
<Text className='icon-rocket'>🚀</Text>
</View>
)}
<View className='card-overlay' />
</View>
<View className='card-body'>
<Text className='card-title'>{item.name}</Text>
<View className='card-header'>
<Text className='card-title'>{item.name}</Text>
<Text className='price'>¥{item.price}</Text>
</View>
<Text className='card-desc'>{item.description}</Text>
<View className='tags'>
<View className='tag cyan'><Text>{item.chip_type}</Text></View>
{item.has_camera && <View className='tag blue'><Text>Camera</Text></View>}
{item.has_microphone && <View className='tag purple'><Text>Mic</Text></View>}
</View>
<View className='card-footer'>
<Text className='price'>¥{item.price}</Text>
<View className='btn-arrow'><Text></Text></View>
<Button className='btn-buy'></Button>
</View>
</View>
</View>
))}
</View>
</ScrollView>
)}
)}
<View className='footer-spacer' />
</View>
</ScrollView>
</View>
)
}

View File

@@ -1,6 +1,6 @@
import Taro from '@tarojs/taro'
const BASE_URL = process.env.TARO_APP_API_URL || 'http://localhost:8000/api'
const BASE_URL = process.env.TARO_APP_API_URL || 'https://market.quant-speed.com/api'
export const request = async (options: Taro.request.Option) => {
const token = Taro.getStorageSync('token')