Merge branch 'main' of local with unrelated histories

This commit is contained in:
jeremygan2021
2026-02-02 22:36:37 +08:00
16 changed files with 579 additions and 104 deletions

View File

@@ -159,6 +159,14 @@ qweasdzxc1
- `POST /api/payments/initiate/` - 发起支付
- `POST /api/payments/confirm/` - 确认支付
## 上传图片接口 不要乱传文件造成oss存储费用增加
### 上传硬件的3D文件小智参数 zip压缩包包含3文件和材质文件
- `POST https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/market_page/hardware_xiaozhi/product_3D_image` - 上传3D文件
### 上传硬件的图片(小智参数) 单张图片
- `POST https://tangledup-ai-staging.oss-cn-shanghai.aliyuncs.com/market_page/hardware_xiaozhi/product_image` - 上传图片
## 🎯 使用说明
### 推广码功能

0
backend/manage.py Executable file → Normal file
View File

View File

@@ -1,6 +1,7 @@
from django.contrib import admin
from django.utils.html import format_html
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
@@ -13,6 +14,37 @@ admin.site.site_header = "量迹AI硬件销售管理后台"
admin.site.site_title = "量迹AI后台"
admin.site.index_title = "欢迎使用量迹AI管理系统"
class ExternalUploadWidget(forms.URLInput):
def __init__(self, upload_url, accept='*', *args, **kwargs):
super().__init__(*args, **kwargs)
self.upload_url = upload_url
self.attrs.update({
'class': 'upload-url-input',
'data-upload-url': upload_url,
'data-accept': accept,
'style': 'width: 100%; margin-bottom: 5px;',
'readonly': 'readonly',
'placeholder': '上传文件后自动生成URL'
})
class Media:
js = ('shop/js/admin_upload.js',)
class ESP32ConfigAdminForm(forms.ModelForm):
class Meta:
model = ESP32Config
fields = '__all__'
widgets = {
'static_image_url': ExternalUploadWidget(
upload_url='https://data.tangledup-ai.com/upload?folder=market_page%2Fhardware_xiaozhi%2Fproduct_static_image',
accept='image/*'
),
'model_3d_url': ExternalUploadWidget(
upload_url='https://data.tangledup-ai.com/upload?folder=market_page%2Fhardware_xiaozhi%2Fproduct_3D_image',
accept='.zip'
),
}
class ProductFeatureInline(TabularInline):
model = ProductFeature
extra = 1
@@ -37,6 +69,7 @@ class WeChatPayConfigAdmin(ModelAdmin):
@admin.register(ESP32Config)
class ESP32ConfigAdmin(ModelAdmin):
form = ESP32ConfigAdminForm
list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone')
list_filter = ('chip_type', 'has_camera')
search_fields = ('name', 'description')
@@ -52,6 +85,10 @@ class ESP32ConfigAdmin(ModelAdmin):
'fields': ('detail_image', 'detail_image_url'),
'description': '图片上传和URL二选一优先使用URL'
}),
('多媒体资源', {
'fields': ('static_image_url', 'model_3d_url'),
'description': '产品静态图和3D模型的外部链接'
}),
)
@admin.register(Service)

View File

@@ -0,0 +1,23 @@
# Generated by Django 6.0.1 on 2026-02-02 11:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0008_service_delivery_content_service_delivery_time_and_more'),
]
operations = [
migrations.AddField(
model_name='esp32config',
name='model_3d_url',
field=models.URLField(blank=True, null=True, verbose_name='产品3D模型 (URL)'),
),
migrations.AddField(
model_name='esp32config',
name='static_image_url',
field=models.URLField(blank=True, null=True, verbose_name='产品静态图 (URL)'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.1 on 2026-02-02 12:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0009_esp32config_model_3d_url_and_more'),
]
operations = [
migrations.AlterField(
model_name='esp32config',
name='model_3d_url',
field=models.URLField(blank=True, help_text='请上传包含 .obj 模型文件和 .mtl 材质文件的 .zip 压缩包', null=True, verbose_name='产品3D模型 (URL)'),
),
]

View File

@@ -19,6 +19,8 @@ class ESP32Config(models.Model):
description = models.TextField(verbose_name="描述", blank=True)
detail_image = models.ImageField(upload_to='products/details/', blank=True, null=True, verbose_name="详情页长图 (上传)")
detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)", help_text="如果填写了URL将优先使用URL")
static_image_url = models.URLField(blank=True, null=True, verbose_name="产品静态图 (URL)")
model_3d_url = models.URLField(blank=True, null=True, verbose_name="产品3D模型 (URL)", help_text="请上传包含 .obj 模型文件和 .mtl 材质文件的 .zip 压缩包")
def __str__(self):
return f"{self.name} - ¥{self.price}"

View File

@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/Quant-Speed_logo.svg" />
<link rel="icon" type="image/svg+xml" href="/liangji_logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
<title>Quant Speed</title>
</head>
<body>
<div id="root"></div>

View File

@@ -15,6 +15,7 @@
"antd": "^6.2.2",
"axios": "^1.13.4",
"framer-motion": "^12.29.2",
"jszip": "^3.10.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0",

View File

@@ -28,9 +28,14 @@ const Layout = ({ children }) => {
icon: <EyeOutlined />,
label: 'AR 体验',
},
{
key: 'more',
label: '...',
},
];
const handleMenuClick = (key) => {
if (key === 'more') return;
navigate(key);
setMobileMenuOpen(false);
};
@@ -57,17 +62,18 @@ const Layout = ({ children }) => {
zIndex: 1000,
width: '100%',
padding: 0,
background: 'rgba(0, 0, 0, 0.5)',
backdropFilter: 'blur(10px)',
background: 'rgba(0, 0, 0, 0.7)',
backdropFilter: 'blur(20px)',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
display: 'flex',
justifyContent: 'center'
height: '72px',
lineHeight: '72px',
boxShadow: '0 4px 30px rgba(0, 0, 0, 0.5)'
}}
>
<div style={{
width: '100%',
maxWidth: '1440px',
padding: '0 20px',
padding: '0 40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
@@ -87,18 +93,24 @@ const Layout = ({ children }) => {
}}
onClick={() => navigate('/')}
>
<img src="/liangji_white.png" alt="Quant-Speed Logo" style={{ height: '32px' }} />
<img src="/liangji_logo.svg" alt="Quant Speed Logo" style={{ height: '40px', filter: 'invert(1) brightness(2)' }} />
</motion.div>
{/* Desktop Menu */}
<div className="desktop-menu" style={{ display: 'none' }}>
<div className="desktop-menu" style={{ display: 'none', flex: 1 }}>
<Menu
theme="dark"
mode="horizontal"
selectedKeys={[location.pathname]}
items={items}
onClick={(e) => handleMenuClick(e.key)}
style={{ background: 'transparent', borderBottom: 'none', minWidth: 300 }}
style={{
background: 'transparent',
borderBottom: 'none',
display: 'flex',
justifyContent: 'flex-end',
minWidth: '400px'
}}
/>
</div>
<style>{`
@@ -136,22 +148,29 @@ const Layout = ({ children }) => {
/>
</Drawer>
<Content style={{ marginTop: 64, padding: '24px', overflowX: 'hidden' }}>
<AnimatePresence mode="wait">
<motion.div
key={location.pathname}
initial={{ opacity: 0, y: 20, filter: 'blur(10px)' }}
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
exit={{ opacity: 0, y: -20, filter: 'blur(10px)' }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>
<Content style={{ marginTop: 72, padding: '40px 20px', overflowX: 'hidden' }}>
<div style={{
maxWidth: '1200px',
margin: '0 auto',
width: '100%',
minHeight: 'calc(100vh - 128px)'
}}>
<AnimatePresence mode="wait">
<motion.div
key={location.pathname}
initial={{ opacity: 0, y: 20, filter: 'blur(10px)' }}
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
exit={{ opacity: 0, y: -20, filter: 'blur(10px)' }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>
</div>
</Content>
<Footer style={{ textAlign: 'center', background: 'rgba(0,0,0,0.5)', color: '#666', backdropFilter: 'blur(5px)' }}>
Quant-Speed AI Hardware ©{new Date().getFullYear()} Created by Quant-Speed Tech
Quant Speed AI Hardware ©{new Date().getFullYear()} Created by Quant Speed Tech
</Footer>
</AntLayout>
</ConfigProvider>

View File

@@ -1,16 +1,21 @@
import React, { Suspense } from 'react';
import React, { Suspense, useState, useEffect } from 'react';
import { Canvas, useLoader } from '@react-three/fiber';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
import { OrbitControls, Stage, useProgress } from '@react-three/drei';
import { OrbitControls, Stage, useProgress, Environment, ContactShadows } from '@react-three/drei';
import { Spin } from 'antd';
import JSZip from 'jszip';
import * as THREE from 'three';
const Model = ({ objPath, mtlPath, scale = 1 }) => {
const materials = useLoader(MTLLoader, mtlPath);
// If mtlPath is provided, load materials first
const materials = mtlPath ? useLoader(MTLLoader, mtlPath) : null;
const obj = useLoader(OBJLoader, objPath, (loader) => {
materials.preload();
loader.setMaterials(materials);
if (materials) {
materials.preload();
loader.setMaterials(materials);
}
});
const clone = obj.clone();
@@ -46,16 +51,127 @@ const LoadingOverlay = () => {
};
const ModelViewer = ({ objPath, mtlPath, scale = 1, autoRotate = true }) => {
const [paths, setPaths] = useState(null);
const [unzipping, setUnzipping] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const blobUrls = [];
const loadPaths = async () => {
if (!objPath) return;
// 如果是 zip 文件
if (objPath.toLowerCase().endsWith('.zip')) {
setUnzipping(true);
setError(null);
try {
const response = await fetch(objPath);
const arrayBuffer = await response.arrayBuffer();
const zip = await JSZip.loadAsync(arrayBuffer);
let extractedObj = null;
let extractedMtl = null;
const fileMap = {};
// 1. 提取所有文件并创建 Blob URL 映射
for (const [filename, file] of Object.entries(zip.files)) {
if (file.dir) continue;
const content = await file.async('blob');
const url = URL.createObjectURL(content);
blobUrls.push(url);
// 记录文件名到 URL 的映射,用于后续材质引用图片等情况
const baseName = filename.split('/').pop();
fileMap[baseName] = url;
if (filename.toLowerCase().endsWith('.obj')) {
extractedObj = url;
} else if (filename.toLowerCase().endsWith('.mtl')) {
extractedMtl = url;
}
}
if (isMounted) {
if (extractedObj) {
setPaths({ obj: extractedObj, mtl: extractedMtl });
} else {
setError('压缩包内未找到 .obj 模型文件');
}
}
} catch (err) {
console.error('Error unzipping model:', err);
if (isMounted) setError('加载压缩包失败');
} finally {
if (isMounted) setUnzipping(false);
}
} else {
// 普通路径
setPaths({ obj: objPath, mtl: mtlPath });
}
};
loadPaths();
return () => {
isMounted = false;
// 清理 Blob URL 释放内存
blobUrls.forEach(url => URL.revokeObjectURL(url));
};
}, [objPath, mtlPath]);
if (unzipping) {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
background: 'rgba(0,0,0,0.1)'
}}>
<Spin size="large" />
<div style={{ color: '#00b96b', marginTop: 15, fontWeight: '500' }}>正在解压 3D 资源...</div>
</div>
);
}
if (error) {
return (
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
color: '#ff4d4f',
padding: 20,
textAlign: 'center'
}}>
{error}
</div>
);
}
if (!paths) return null;
return (
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<LoadingOverlay />
<Canvas shadows dpr={[1, 2]} camera={{ fov: 45 }} style={{ height: '100%', width: '100%' }}>
<Canvas shadows dpr={[1, 2]} camera={{ fov: 45, position: [0, 0, 5] }} style={{ height: '100%', width: '100%' }}>
<ambientLight intensity={0.7} />
<pointLight position={[10, 10, 10]} intensity={1} />
<spotLight position={[-10, 10, 10]} angle={0.15} penumbra={1} intensity={1} />
<Suspense fallback={null}>
<Stage environment="city" intensity={0.5}>
<Model objPath={objPath} mtlPath={mtlPath} scale={scale} />
<Stage environment="city" intensity={0.6} adjustCamera={true}>
<Model objPath={paths.obj} mtlPath={paths.mtl} scale={scale} />
</Stage>
<Environment preset="city" />
<ContactShadows position={[0, -0.8, 0]} opacity={0.4} scale={10} blur={2} far={0.8} />
</Suspense>
<OrbitControls autoRotate={autoRotate} />
<OrbitControls autoRotate={autoRotate} makeDefault />
</Canvas>
</div>
);

View File

@@ -18,6 +18,8 @@ const ParticleBackground = () => {
const particles = [];
const particleCount = 100;
const meteors = [];
const meteorCount = 8;
class Particle {
constructor() {
@@ -45,13 +47,93 @@ const ParticleBackground = () => {
}
}
class Meteor {
constructor() {
this.reset();
}
reset() {
this.x = Math.random() * canvas.width * 1.5; // Start further right
this.y = Math.random() * -canvas.height; // Start further above
this.vx = -(Math.random() * 5 + 5); // Faster
this.vy = Math.random() * 5 + 5; // Faster
this.len = Math.random() * 150 + 150; // Longer trail
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() * 300; // Random delay before showing up
}
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 > canvas.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(0.1, this.color + (this.opacity * 0.5) + ')');
gradient.addColorStop(1, this.color + '0)');
ctx.save();
// Add glow effect
ctx.shadowBlur = 8;
ctx.shadowColor = this.color.replace('rgba', 'rgb').replace(', ', ')');
ctx.beginPath();
ctx.strokeStyle = gradient;
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.moveTo(this.x, this.y);
ctx.lineTo(tailX, tailY);
ctx.stroke();
// Add a bright head
ctx.beginPath();
ctx.fillStyle = '#fff';
ctx.arc(this.x, this.y, 1, 0, Math.PI * 2);
ctx.fill();
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, canvas.width, canvas.height);
// Draw meteors first (in background)
meteors.forEach(m => {
m.update();
m.draw();
});
// Draw connecting lines
ctx.lineWidth = 0.5;
for (let i = 0; i < particleCount; i++) {

View File

@@ -1,7 +1,14 @@
import React, { useEffect, useState } from 'react';
import { Row, Col, Typography, Button, Spin } from 'antd';
import { motion } from 'framer-motion';
import { RightOutlined } from '@ant-design/icons';
import {
RightOutlined,
SearchOutlined,
DatabaseOutlined,
ThunderboltOutlined,
CheckCircleOutlined,
CloudServerOutlined
} from '@ant-design/icons';
import { getServices } from '../api';
import { useNavigate } from 'react-router-dom';
@@ -127,41 +134,96 @@ const AIServices = () => {
))}
</Row>
{/* 动态流程图模拟 */}
{/* 动态流程图优化 */}
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
style={{ marginTop: 80, padding: 40, background: 'rgba(0,0,0,0.3)', borderRadius: 20, border: '1px dashed #333', textAlign: 'center' }}
transition={{ duration: 1 }}
style={{
marginTop: 100,
padding: '60px 20px',
background: 'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,185,107,0.05) 100%)',
borderRadius: 30,
border: '1px solid rgba(255,255,255,0.05)',
position: 'relative',
overflow: 'hidden'
}}
>
<Title level={3} style={{ color: '#fff', marginBottom: 40 }}>服务流程</Title>
<Row justify="space-around" align="middle" gutter={[20, 20]}>
{['需求分析', '数据准备', '模型训练', '测试验证', '私有化部署'].map((step, i) => (
<Col key={i} xs={12} md={4}>
<div style={{
width: '100%', aspectRatio: '1',
border: '2px solid #333', borderRadius: '50%',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#888', fontSize: 16, fontWeight: 'bold',
position: 'relative'
}}>
{step}
{/* 简单的连接线模拟 */}
{i < 4 && (
<div className="process-arrow" style={{
position: 'absolute', right: -20, top: '50%',
width: 20, height: 2, background: '#333',
display: 'none' // 移动端隐藏
}} />
)}
</div>
</Col>
))}
<div style={{ position: 'absolute', top: -50, right: -50, width: 200, height: 200, background: 'radial-gradient(circle, rgba(0,240,255,0.1) 0%, transparent 70%)', filter: 'blur(30px)' }} />
<Title level={2} style={{ color: '#fff', marginBottom: 60, textAlign: 'center' }}>
<span className="neon-text-green">服务流程</span>
</Title>
<Row justify="center" gutter={[0, 40]} style={{ position: 'relative' }}>
{[
{ title: '需求分析', icon: <SearchOutlined />, desc: '深度沟通需求' },
{ title: '数据准备', icon: <DatabaseOutlined />, desc: '高效数据处理' },
{ title: '模型训练', icon: <ThunderboltOutlined />, desc: '高性能算力' },
{ title: '测试验证', icon: <CheckCircleOutlined />, desc: '多维精度测试' },
{ title: '私有化部署', icon: <CloudServerOutlined />, desc: '全栈落地部署' }
].map((step, i) => (
<Col key={i} xs={24} sm={12} md={4}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', position: 'relative' }}>
<motion.div
initial={{ scale: 0, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
transition={{ delay: i * 0.2, type: 'spring', stiffness: 100 }}
whileHover={{ y: -10 }}
style={{
width: 80,
height: 80,
borderRadius: '24px',
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(0, 185, 107, 0.3)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 32,
color: '#00b96b',
marginBottom: 20,
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
backdropFilter: 'blur(10px)',
zIndex: 2
}}
>
{step.icon}
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.2 + 0.3 }}
>
<div style={{ color: '#fff', fontSize: 18, fontWeight: 'bold', marginBottom: 8 }}>{step.title}</div>
<div style={{ color: '#666', fontSize: 12 }}>{step.desc}</div>
</motion.div>
{/* 连接线 */}
{i < 4 && (
<div className="process-line" style={{
position: 'absolute',
top: 40,
right: '-50%',
width: '100%',
height: '2px',
background: 'linear-gradient(90deg, #00b96b33, #00b96b00)',
zIndex: 1,
display: 'none'
}} />
)}
</div>
</Col>
))}
</Row>
<style>{`
@media (min-width: 768px) {
.process-arrow { display: block !important; }
.process-line { display: block !important; }
}
.neon-text-green {
text-shadow: 0 0 10px rgba(0, 185, 107, 0.5);
}
`}</style>
</motion.div>

View File

@@ -3,14 +3,22 @@
border: 1px solid #303030 !important;
transition: all 0.3s ease;
cursor: pointer;
box-shadow: none !important; /* 强制移除默认阴影 */
overflow: hidden; /* 确保子元素不会溢出产生黑边 */
outline: none;
}
.tech-card:hover {
border-color: #00b96b !important;
box-shadow: 0 0 15px rgba(0, 185, 107, 0.3);
box-shadow: 0 0 20px rgba(0, 185, 107, 0.4) !important; /* 增强悬停发光 */
transform: translateY(-5px);
}
.tech-card .ant-card-body {
border-top: none !important;
box-shadow: none !important;
}
.tech-card-title {
color: #fff;
font-size: 18px;
@@ -23,3 +31,48 @@
font-size: 20px;
font-weight: bold;
}
.product-scroll-container {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
padding: 30px 20px; /* 增加左右内边距,为悬停缩放和投影留出空间 */
margin: 0 -20px; /* 使用负外边距抵消内边距,使滚动条能延伸到版心边缘 */
width: calc(100% + 40px);
}
/* 自定义滚动条 */
.product-scroll-container::-webkit-scrollbar {
height: 6px;
}
.product-scroll-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
margin: 0 20px; /* 让滚动条轨道在版心内显示 */
}
.product-scroll-container::-webkit-scrollbar-thumb {
background: rgba(0, 185, 107, 0.2);
border-radius: 3px;
transition: all 0.3s;
}
.product-scroll-container::-webkit-scrollbar-thumb:hover {
background: rgba(0, 185, 107, 0.5);
}
/* 布局对齐 */
.product-scroll-container .ant-row {
margin-left: 0 !important;
margin-right: 0 !important;
padding: 0;
display: flex;
flex-wrap: nowrap;
justify-content: flex-start;
}
.product-scroll-container .ant-col {
flex: 0 0 320px;
padding: 0 12px;
}

View File

@@ -12,6 +12,7 @@ const Home = () => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [typedText, setTypedText] = useState('');
const [isTypingComplete, setIsTypingComplete] = useState(false);
const fullText = "未来已来 AI 核心驱动";
const navigate = useNavigate();
@@ -23,6 +24,7 @@ const Home = () => {
setTypedText(fullText.slice(0, i));
if (i >= fullText.length) {
clearInterval(typingInterval);
setIsTypingComplete(true);
}
}, 150);
@@ -56,7 +58,6 @@ const Home = () => {
scale: 1.05,
rotateX: 5,
rotateY: 5,
boxShadow: "0px 10px 30px rgba(0, 185, 107, 0.4)",
transition: { duration: 0.3 }
}
};
@@ -71,7 +72,7 @@ const Home = () => {
return (
<div>
<div style={{ textAlign: 'center', marginBottom: 60, marginTop: 40 }}>
<div style={{ textAlign: 'center', marginBottom: 60 }}>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
@@ -80,12 +81,12 @@ const Home = () => {
>
<motion.img
src="/gXEu5E01.svg"
alt="LiangJi Tech Logo"
alt="Quant Speed Logo"
animate={{
filter: [
'invert(1) drop-shadow(0 0 10px rgba(0, 240, 255, 0.3))',
'invert(1) drop-shadow(0 0 20px rgba(0, 240, 255, 0.7))',
'invert(1) drop-shadow(0 0 10px rgba(0, 240, 255, 0.3))'
'invert(1) brightness(2) drop-shadow(0 0 10px rgba(0, 240, 255, 0.3))',
'invert(1) brightness(2) drop-shadow(0 0 20px rgba(0, 240, 255, 0.7))',
'invert(1) brightness(2) drop-shadow(0 0 10px rgba(0, 240, 255, 0.3))'
]
}}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
@@ -93,7 +94,8 @@ const Home = () => {
/>
</motion.div>
<Title level={1} style={{ color: '#fff', fontSize: 'clamp(2rem, 5vw, 4rem)', marginBottom: 20, minHeight: '60px' }}>
<span className="neon-text-green">{typedText}</span><span className="cursor-blink">|</span>
<span className="neon-text-green">{typedText}</span>
{!isTypingComplete && <span className="cursor-blink">|</span>}
</Title>
<motion.div
initial={{ opacity: 0 }}
@@ -106,17 +108,18 @@ const Home = () => {
</motion.div>
</div>
<Row gutter={[24, 24]}>
{products.map((product, index) => (
<Col xs={24} sm={12} md={8} lg={6} key={product.id}>
<motion.div
custom={index}
initial="hidden"
animate="visible"
whileHover="hover"
variants={cardVariants}
style={{ perspective: 1000 }}
>
<div className="product-scroll-container">
<Row gutter={[24, 24]} wrap={false}>
{products.map((product, index) => (
<Col key={product.id} flex="0 0 320px">
<motion.div
custom={index}
initial="hidden"
animate="visible"
whileHover="hover"
variants={cardVariants}
style={{ perspective: 1000 }}
>
<Card
className="tech-card glass-panel"
bordered={false}
@@ -128,14 +131,23 @@ const Home = () => {
justifyContent: 'center',
alignItems: 'center',
color: '#444',
borderBottom: '1px solid rgba(255,255,255,0.05)'
borderBottom: '1px solid rgba(255,255,255,0.05)',
overflow: 'hidden'
}}>
<motion.div
animate={{ y: [0, -10, 0] }}
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
>
<RocketOutlined style={{ fontSize: 60, color: '#00b96b' }} />
</motion.div>
{product.static_image_url ? (
<img
src={product.static_image_url}
alt={product.name}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
<motion.div
animate={{ y: [0, -10, 0] }}
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
>
<RocketOutlined style={{ fontSize: 60, color: '#00b96b' }} />
</motion.div>
)}
</div>
}
onClick={() => navigate(`/product/${product.id}`)}
@@ -144,9 +156,10 @@ const Home = () => {
<div style={{ marginBottom: 10, height: 40, overflow: 'hidden', color: '#bbb' }}>
{product.description}
</div>
<div style={{ marginBottom: 15 }}>
<Tag color="cyan" style={{ background: 'rgba(0,255,255,0.1)', border: '1px solid cyan' }}>{product.chip_type}</Tag>
{product.has_camera && <Tag color="blue" style={{ background: 'rgba(0,0,255,0.1)', border: '1px solid blue' }}>Camera</Tag>}
<div style={{ marginBottom: 15, display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
<Tag color="cyan" style={{ background: 'rgba(0,255,255,0.1)', border: '1px solid cyan', margin: 0 }}>{product.chip_type}</Tag>
{product.has_camera && <Tag color="blue" style={{ background: 'rgba(0,0,255,0.1)', border: '1px solid blue', margin: 0 }}>Camera</Tag>}
{product.has_microphone && <Tag color="purple" style={{ background: 'rgba(114,46,209,0.1)', border: '1px solid #722ed1', margin: 0 }}>Mic</Tag>}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="tech-price neon-text-green">¥{product.price}</div>
@@ -157,6 +170,7 @@ const Home = () => {
</Col>
))}
</Row>
</div>
<style>{`
.cursor-blink {
animation: blink 1s step-end infinite;

View File

@@ -64,6 +64,17 @@ const ProductDetail = () => {
const getModelPaths = (p) => {
if (!p) return null;
// 优先使用后台配置的 3D 模型 URL
if (p.model_3d_url) {
return { obj: p.model_3d_url };
}
// 如果有静态图,且没有特定的 3D 模型 URL则优先显示静态图不进入下方的通用 3D 模板匹配
if (p.static_image_url) {
return null;
}
const text = (p.name + p.description).toLowerCase();
if (text.includes('mini')) {
@@ -101,7 +112,7 @@ const ProductDetail = () => {
if (!product) return null;
return (
<div className="product-detail-container">
<div className="product-detail-container" style={{ paddingBottom: '60px' }}>
{/* Hero Section */}
<Row gutter={40} align="middle" style={{ minHeight: '60vh' }}>
<Col xs={24} md={12}>
@@ -117,6 +128,8 @@ const ProductDetail = () => {
}}>
{modelPaths ? (
<ModelViewer objPath={modelPaths.obj} mtlPath={modelPaths.mtl} />
) : product.static_image_url ? (
<img src={product.static_image_url} alt={product.name} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} />
) : (
<ThunderboltOutlined style={{ fontSize: 120, color: '#00b96b' }} />
)}
@@ -126,10 +139,10 @@ const ProductDetail = () => {
<h1 style={{ fontSize: 48, fontWeight: 'bold', color: '#fff' }}>{product.name}</h1>
<p style={{ fontSize: 20, color: '#888', margin: '20px 0' }}>{product.description}</p>
<div style={{ marginBottom: 30 }}>
<span className="spec-tag">{product.chip_type}</span>
{product.has_camera && <span className="spec-tag">高清摄像头</span>}
{product.has_microphone && <span className="spec-tag">阵列麦克风</span>}
<div style={{ marginBottom: 30, display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
<Tag color="cyan" style={{ background: 'rgba(0,255,255,0.1)', border: '1px solid cyan', padding: '4px 12px', fontSize: '14px', margin: 0 }}>{product.chip_type}</Tag>
{product.has_camera && <Tag color="blue" style={{ background: 'rgba(0,0,255,0.1)', border: '1px solid blue', padding: '4px 12px', fontSize: '14px', margin: 0 }}>高清摄像头</Tag>}
{product.has_microphone && <Tag color="purple" style={{ background: 'rgba(114,46,209,0.1)', border: '1px solid #722ed1', padding: '4px 12px', fontSize: '14px', margin: 0 }}>阵列麦克风</Tag>}
</div>
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 20, marginBottom: 40 }}>

View File

@@ -72,7 +72,7 @@ const ServiceDetail = () => {
}
return (
<div style={{ padding: '20px 0', maxWidth: 1200, margin: '0 auto' }}>
<div style={{ padding: '20px 0' }}>
<Button
type="text"
icon={<ArrowLeftOutlined />}
@@ -97,13 +97,28 @@ const ServiceDetail = () => {
{service.description}
</Paragraph>
<div style={{ marginTop: 30, background: 'rgba(255,255,255,0.05)', padding: 20, borderRadius: 12 }}>
<Title level={4} style={{ color: '#fff' }}>服务详情</Title>
<Descriptions column={1} labelStyle={{ color: '#888' }} contentStyle={{ color: '#fff' }}>
<Descriptions.Item label={<span style={{ display: 'flex', alignItems: 'center' }}><ClockCircleOutlined style={{ marginRight: 8 }} /> 交付周期</span>}>
<div style={{
marginTop: 30,
background: 'rgba(255,255,255,0.03)',
padding: '24px',
borderRadius: 16,
border: '1px solid rgba(255,255,255,0.08)',
backdropFilter: 'blur(10px)',
boxShadow: '0 8px 32px rgba(0,0,0,0.2)'
}}>
<Title level={4} style={{ color: '#fff', marginBottom: 20, display: 'flex', alignItems: 'center' }}>
<div style={{ width: 4, height: 18, background: service.color, marginRight: 10, borderRadius: 2 }} />
服务详情
</Title>
<Descriptions
column={1}
labelStyle={{ color: '#888', fontWeight: 'normal' }}
contentStyle={{ color: '#fff', fontWeight: '500' }}
>
<Descriptions.Item label={<span style={{ display: 'flex', alignItems: 'center' }}><ClockCircleOutlined style={{ marginRight: 8, color: service.color }} /> 交付周期</span>}>
{service.delivery_time || '待沟通'}
</Descriptions.Item>
<Descriptions.Item label={<span style={{ display: 'flex', alignItems: 'center' }}><GiftOutlined style={{ marginRight: 8 }} /> 交付内容</span>}>
<Descriptions.Item label={<span style={{ display: 'flex', alignItems: 'center' }}><GiftOutlined style={{ marginRight: 8, color: service.color }} /> 交付内容</span>}>
{service.delivery_content || '根据需求定制'}
</Descriptions.Item>
</Descriptions>
@@ -147,9 +162,21 @@ const ServiceDetail = () => {
<span style={{ color: '#888', marginLeft: 8 }}>/ {service.unit} </span>
</div>
<div style={{ marginBottom: 20 }}>
<div style={{ marginBottom: 25, display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
{service.features_list && service.features_list.map((feat, i) => (
<Tag color={service.color} key={i} style={{ marginBottom: 8, padding: '4px 10px' }}>
<Tag
key={i}
style={{
margin: 0,
padding: '4px 12px',
background: `${service.color}11`,
color: service.color,
border: `1px solid ${service.color}66`,
borderRadius: '6px',
fontSize: '14px',
backdropFilter: 'blur(4px)'
}}
>
{feat}
</Tag>
))}