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/initiate/` - 发起支付
- `POST /api/payments/confirm/` - 确认支付 - `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.contrib import admin
from django.utils.html import format_html from django.utils.html import format_html
from django.db.models import Sum from django.db.models import Sum
from django import forms
from unfold.admin import ModelAdmin, TabularInline from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import display from unfold.decorators import display
from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService, ProductFeature 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.site_title = "量迹AI后台"
admin.site.index_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): class ProductFeatureInline(TabularInline):
model = ProductFeature model = ProductFeature
extra = 1 extra = 1
@@ -37,6 +69,7 @@ class WeChatPayConfigAdmin(ModelAdmin):
@admin.register(ESP32Config) @admin.register(ESP32Config)
class ESP32ConfigAdmin(ModelAdmin): class ESP32ConfigAdmin(ModelAdmin):
form = ESP32ConfigAdminForm
list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone') list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone')
list_filter = ('chip_type', 'has_camera') list_filter = ('chip_type', 'has_camera')
search_fields = ('name', 'description') search_fields = ('name', 'description')
@@ -52,6 +85,10 @@ class ESP32ConfigAdmin(ModelAdmin):
'fields': ('detail_image', 'detail_image_url'), 'fields': ('detail_image', 'detail_image_url'),
'description': '图片上传和URL二选一优先使用URL' 'description': '图片上传和URL二选一优先使用URL'
}), }),
('多媒体资源', {
'fields': ('static_image_url', 'model_3d_url'),
'description': '产品静态图和3D模型的外部链接'
}),
) )
@admin.register(Service) @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) description = models.TextField(verbose_name="描述", blank=True)
detail_image = models.ImageField(upload_to='products/details/', blank=True, null=True, verbose_name="详情页长图 (上传)") 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") 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): def __str__(self):
return f"{self.name} - ¥{self.price}" return f"{self.name} - ¥{self.price}"

View File

@@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title> <title>Quant Speed</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

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

View File

@@ -28,9 +28,14 @@ const Layout = ({ children }) => {
icon: <EyeOutlined />, icon: <EyeOutlined />,
label: 'AR 体验', label: 'AR 体验',
}, },
{
key: 'more',
label: '...',
},
]; ];
const handleMenuClick = (key) => { const handleMenuClick = (key) => {
if (key === 'more') return;
navigate(key); navigate(key);
setMobileMenuOpen(false); setMobileMenuOpen(false);
}; };
@@ -57,17 +62,18 @@ const Layout = ({ children }) => {
zIndex: 1000, zIndex: 1000,
width: '100%', width: '100%',
padding: 0, padding: 0,
background: 'rgba(0, 0, 0, 0.5)', background: 'rgba(0, 0, 0, 0.7)',
backdropFilter: 'blur(10px)', backdropFilter: 'blur(20px)',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)', borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
display: 'flex', display: 'flex',
justifyContent: 'center' height: '72px',
lineHeight: '72px',
boxShadow: '0 4px 30px rgba(0, 0, 0, 0.5)'
}} }}
> >
<div style={{ <div style={{
width: '100%', width: '100%',
maxWidth: '1440px', padding: '0 40px',
padding: '0 20px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
@@ -87,18 +93,24 @@ const Layout = ({ children }) => {
}} }}
onClick={() => navigate('/')} 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> </motion.div>
{/* Desktop Menu */} {/* Desktop Menu */}
<div className="desktop-menu" style={{ display: 'none' }}> <div className="desktop-menu" style={{ display: 'none', flex: 1 }}>
<Menu <Menu
theme="dark" theme="dark"
mode="horizontal" mode="horizontal"
selectedKeys={[location.pathname]} selectedKeys={[location.pathname]}
items={items} items={items}
onClick={(e) => handleMenuClick(e.key)} 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> </div>
<style>{` <style>{`
@@ -136,22 +148,29 @@ const Layout = ({ children }) => {
/> />
</Drawer> </Drawer>
<Content style={{ marginTop: 64, padding: '24px', overflowX: 'hidden' }}> <Content style={{ marginTop: 72, padding: '40px 20px', overflowX: 'hidden' }}>
<AnimatePresence mode="wait"> <div style={{
<motion.div maxWidth: '1200px',
key={location.pathname} margin: '0 auto',
initial={{ opacity: 0, y: 20, filter: 'blur(10px)' }} width: '100%',
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }} minHeight: 'calc(100vh - 128px)'
exit={{ opacity: 0, y: -20, filter: 'blur(10px)' }} }}>
transition={{ duration: 0.3 }} <AnimatePresence mode="wait">
> <motion.div
{children} key={location.pathname}
</motion.div> initial={{ opacity: 0, y: 20, filter: 'blur(10px)' }}
</AnimatePresence> 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> </Content>
<Footer style={{ textAlign: 'center', background: 'rgba(0,0,0,0.5)', color: '#666', backdropFilter: 'blur(5px)' }}> <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> </Footer>
</AntLayout> </AntLayout>
</ConfigProvider> </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 { Canvas, useLoader } from '@react-three/fiber';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'; import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'; 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 { Spin } from 'antd';
import JSZip from 'jszip';
import * as THREE from 'three';
const Model = ({ objPath, mtlPath, scale = 1 }) => { 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) => { const obj = useLoader(OBJLoader, objPath, (loader) => {
materials.preload(); if (materials) {
loader.setMaterials(materials); materials.preload();
loader.setMaterials(materials);
}
}); });
const clone = obj.clone(); const clone = obj.clone();
@@ -46,16 +51,127 @@ const LoadingOverlay = () => {
}; };
const ModelViewer = ({ objPath, mtlPath, scale = 1, autoRotate = true }) => { 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 ( return (
<div style={{ position: 'relative', width: '100%', height: '100%' }}> <div style={{ position: 'relative', width: '100%', height: '100%' }}>
<LoadingOverlay /> <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}> <Suspense fallback={null}>
<Stage environment="city" intensity={0.5}> <Stage environment="city" intensity={0.6} adjustCamera={true}>
<Model objPath={objPath} mtlPath={mtlPath} scale={scale} /> <Model objPath={paths.obj} mtlPath={paths.mtl} scale={scale} />
</Stage> </Stage>
<Environment preset="city" />
<ContactShadows position={[0, -0.8, 0]} opacity={0.4} scale={10} blur={2} far={0.8} />
</Suspense> </Suspense>
<OrbitControls autoRotate={autoRotate} /> <OrbitControls autoRotate={autoRotate} makeDefault />
</Canvas> </Canvas>
</div> </div>
); );

View File

@@ -18,6 +18,8 @@ const ParticleBackground = () => {
const particles = []; const particles = [];
const particleCount = 100; const particleCount = 100;
const meteors = [];
const meteorCount = 8;
class Particle { class Particle {
constructor() { 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++) { for (let i = 0; i < particleCount; i++) {
particles.push(new Particle()); particles.push(new Particle());
} }
for (let i = 0; i < meteorCount; i++) {
meteors.push(new Meteor());
}
const animate = () => { const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw meteors first (in background)
meteors.forEach(m => {
m.update();
m.draw();
});
// Draw connecting lines // Draw connecting lines
ctx.lineWidth = 0.5; ctx.lineWidth = 0.5;
for (let i = 0; i < particleCount; i++) { for (let i = 0; i < particleCount; i++) {

View File

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

View File

@@ -3,14 +3,22 @@
border: 1px solid #303030 !important; border: 1px solid #303030 !important;
transition: all 0.3s ease; transition: all 0.3s ease;
cursor: pointer; cursor: pointer;
box-shadow: none !important; /* 强制移除默认阴影 */
overflow: hidden; /* 确保子元素不会溢出产生黑边 */
outline: none;
} }
.tech-card:hover { .tech-card:hover {
border-color: #00b96b !important; 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); transform: translateY(-5px);
} }
.tech-card .ant-card-body {
border-top: none !important;
box-shadow: none !important;
}
.tech-card-title { .tech-card-title {
color: #fff; color: #fff;
font-size: 18px; font-size: 18px;
@@ -23,3 +31,48 @@
font-size: 20px; font-size: 20px;
font-weight: bold; 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 [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [typedText, setTypedText] = useState(''); const [typedText, setTypedText] = useState('');
const [isTypingComplete, setIsTypingComplete] = useState(false);
const fullText = "未来已来 AI 核心驱动"; const fullText = "未来已来 AI 核心驱动";
const navigate = useNavigate(); const navigate = useNavigate();
@@ -23,6 +24,7 @@ const Home = () => {
setTypedText(fullText.slice(0, i)); setTypedText(fullText.slice(0, i));
if (i >= fullText.length) { if (i >= fullText.length) {
clearInterval(typingInterval); clearInterval(typingInterval);
setIsTypingComplete(true);
} }
}, 150); }, 150);
@@ -56,7 +58,6 @@ const Home = () => {
scale: 1.05, scale: 1.05,
rotateX: 5, rotateX: 5,
rotateY: 5, rotateY: 5,
boxShadow: "0px 10px 30px rgba(0, 185, 107, 0.4)",
transition: { duration: 0.3 } transition: { duration: 0.3 }
} }
}; };
@@ -71,7 +72,7 @@ const Home = () => {
return ( return (
<div> <div>
<div style={{ textAlign: 'center', marginBottom: 60, marginTop: 40 }}> <div style={{ textAlign: 'center', marginBottom: 60 }}>
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.8 }} initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
@@ -80,12 +81,12 @@ const Home = () => {
> >
<motion.img <motion.img
src="/gXEu5E01.svg" src="/gXEu5E01.svg"
alt="LiangJi Tech Logo" alt="Quant Speed Logo"
animate={{ animate={{
filter: [ filter: [
'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) drop-shadow(0 0 20px rgba(0, 240, 255, 0.7))', 'invert(1) brightness(2) 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))'
] ]
}} }}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }} transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
@@ -93,7 +94,8 @@ const Home = () => {
/> />
</motion.div> </motion.div>
<Title level={1} style={{ color: '#fff', fontSize: 'clamp(2rem, 5vw, 4rem)', marginBottom: 20, minHeight: '60px' }}> <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> </Title>
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
@@ -106,17 +108,18 @@ const Home = () => {
</motion.div> </motion.div>
</div> </div>
<Row gutter={[24, 24]}> <div className="product-scroll-container">
{products.map((product, index) => ( <Row gutter={[24, 24]} wrap={false}>
<Col xs={24} sm={12} md={8} lg={6} key={product.id}> {products.map((product, index) => (
<motion.div <Col key={product.id} flex="0 0 320px">
custom={index} <motion.div
initial="hidden" custom={index}
animate="visible" initial="hidden"
whileHover="hover" animate="visible"
variants={cardVariants} whileHover="hover"
style={{ perspective: 1000 }} variants={cardVariants}
> style={{ perspective: 1000 }}
>
<Card <Card
className="tech-card glass-panel" className="tech-card glass-panel"
bordered={false} bordered={false}
@@ -128,14 +131,23 @@ const Home = () => {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
color: '#444', color: '#444',
borderBottom: '1px solid rgba(255,255,255,0.05)' borderBottom: '1px solid rgba(255,255,255,0.05)',
overflow: 'hidden'
}}> }}>
<motion.div {product.static_image_url ? (
animate={{ y: [0, -10, 0] }} <img
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }} src={product.static_image_url}
> alt={product.name}
<RocketOutlined style={{ fontSize: 60, color: '#00b96b' }} /> style={{ width: '100%', height: '100%', objectFit: 'cover' }}
</motion.div> />
) : (
<motion.div
animate={{ y: [0, -10, 0] }}
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
>
<RocketOutlined style={{ fontSize: 60, color: '#00b96b' }} />
</motion.div>
)}
</div> </div>
} }
onClick={() => navigate(`/product/${product.id}`)} onClick={() => navigate(`/product/${product.id}`)}
@@ -144,9 +156,10 @@ const Home = () => {
<div style={{ marginBottom: 10, height: 40, overflow: 'hidden', color: '#bbb' }}> <div style={{ marginBottom: 10, height: 40, overflow: 'hidden', color: '#bbb' }}>
{product.description} {product.description}
</div> </div>
<div style={{ marginBottom: 15 }}> <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' }}>{product.chip_type}</Tag> <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' }}>Camera</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>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="tech-price neon-text-green">¥{product.price}</div> <div className="tech-price neon-text-green">¥{product.price}</div>
@@ -157,6 +170,7 @@ const Home = () => {
</Col> </Col>
))} ))}
</Row> </Row>
</div>
<style>{` <style>{`
.cursor-blink { .cursor-blink {
animation: blink 1s step-end infinite; animation: blink 1s step-end infinite;

View File

@@ -64,6 +64,17 @@ const ProductDetail = () => {
const getModelPaths = (p) => { const getModelPaths = (p) => {
if (!p) return null; 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(); const text = (p.name + p.description).toLowerCase();
if (text.includes('mini')) { if (text.includes('mini')) {
@@ -101,7 +112,7 @@ const ProductDetail = () => {
if (!product) return null; if (!product) return null;
return ( return (
<div className="product-detail-container"> <div className="product-detail-container" style={{ paddingBottom: '60px' }}>
{/* Hero Section */} {/* Hero Section */}
<Row gutter={40} align="middle" style={{ minHeight: '60vh' }}> <Row gutter={40} align="middle" style={{ minHeight: '60vh' }}>
<Col xs={24} md={12}> <Col xs={24} md={12}>
@@ -117,6 +128,8 @@ const ProductDetail = () => {
}}> }}>
{modelPaths ? ( {modelPaths ? (
<ModelViewer objPath={modelPaths.obj} mtlPath={modelPaths.mtl} /> <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' }} /> <ThunderboltOutlined style={{ fontSize: 120, color: '#00b96b' }} />
)} )}
@@ -126,10 +139,10 @@ const ProductDetail = () => {
<h1 style={{ fontSize: 48, fontWeight: 'bold', color: '#fff' }}>{product.name}</h1> <h1 style={{ fontSize: 48, fontWeight: 'bold', color: '#fff' }}>{product.name}</h1>
<p style={{ fontSize: 20, color: '#888', margin: '20px 0' }}>{product.description}</p> <p style={{ fontSize: 20, color: '#888', margin: '20px 0' }}>{product.description}</p>
<div style={{ marginBottom: 30 }}> <div style={{ marginBottom: 30, display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
<span className="spec-tag">{product.chip_type}</span> <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 && <span className="spec-tag">高清摄像头</span>} {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 && <span className="spec-tag">阵列麦克风</span>} {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>
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 20, marginBottom: 40 }}> <div style={{ display: 'flex', alignItems: 'flex-end', gap: 20, marginBottom: 40 }}>

View File

@@ -72,7 +72,7 @@ const ServiceDetail = () => {
} }
return ( return (
<div style={{ padding: '20px 0', maxWidth: 1200, margin: '0 auto' }}> <div style={{ padding: '20px 0' }}>
<Button <Button
type="text" type="text"
icon={<ArrowLeftOutlined />} icon={<ArrowLeftOutlined />}
@@ -97,13 +97,28 @@ const ServiceDetail = () => {
{service.description} {service.description}
</Paragraph> </Paragraph>
<div style={{ marginTop: 30, background: 'rgba(255,255,255,0.05)', padding: 20, borderRadius: 12 }}> <div style={{
<Title level={4} style={{ color: '#fff' }}>服务详情</Title> marginTop: 30,
<Descriptions column={1} labelStyle={{ color: '#888' }} contentStyle={{ color: '#fff' }}> background: 'rgba(255,255,255,0.03)',
<Descriptions.Item label={<span style={{ display: 'flex', alignItems: 'center' }}><ClockCircleOutlined style={{ marginRight: 8 }} /> 交付周期</span>}> 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 || '待沟通'} {service.delivery_time || '待沟通'}
</Descriptions.Item> </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 || '根据需求定制'} {service.delivery_content || '根据需求定制'}
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
@@ -147,9 +162,21 @@ const ServiceDetail = () => {
<span style={{ color: '#888', marginLeft: 8 }}>/ {service.unit} </span> <span style={{ color: '#888', marginLeft: 8 }}>/ {service.unit} </span>
</div> </div>
<div style={{ marginBottom: 20 }}> <div style={{ marginBottom: 25, display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
{service.features_list && service.features_list.map((feat, i) => ( {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} {feat}
</Tag> </Tag>
))} ))}