Merge branch 'main' of local with unrelated histories
This commit is contained in:
@@ -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
0
backend/manage.py
Executable file → Normal 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)
|
||||
|
||||
@@ -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)'),
|
||||
),
|
||||
]
|
||||
@@ -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)'),
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user