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/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
0
backend/manage.py
Executable file → Normal 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)
|
||||||
|
|||||||
@@ -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)
|
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}"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,7 +148,13 @@ const Layout = ({ children }) => {
|
|||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<Content style={{ marginTop: 64, padding: '24px', overflowX: 'hidden' }}>
|
<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">
|
<AnimatePresence mode="wait">
|
||||||
<motion.div
|
<motion.div
|
||||||
key={location.pathname}
|
key={location.pathname}
|
||||||
@@ -148,10 +166,11 @@ const Layout = ({ children }) => {
|
|||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
if (materials) {
|
||||||
materials.preload();
|
materials.preload();
|
||||||
loader.setMaterials(materials);
|
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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++) {
|
||||||
|
|||||||
@@ -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: '全栈落地部署' }
|
||||||
|
].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 && (
|
{i < 4 && (
|
||||||
<div className="process-arrow" style={{
|
<div className="process-line" style={{
|
||||||
position: 'absolute', right: -20, top: '50%',
|
position: 'absolute',
|
||||||
width: 20, height: 2, background: '#333',
|
top: 40,
|
||||||
display: 'none' // 移动端隐藏
|
right: '-50%',
|
||||||
|
width: '100%',
|
||||||
|
height: '2px',
|
||||||
|
background: 'linear-gradient(90deg, #00b96b33, #00b96b00)',
|
||||||
|
zIndex: 1,
|
||||||
|
display: 'none'
|
||||||
}} />
|
}} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,9 +108,10 @@ const Home = () => {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Row gutter={[24, 24]}>
|
<div className="product-scroll-container">
|
||||||
|
<Row gutter={[24, 24]} wrap={false}>
|
||||||
{products.map((product, index) => (
|
{products.map((product, index) => (
|
||||||
<Col xs={24} sm={12} md={8} lg={6} key={product.id}>
|
<Col key={product.id} flex="0 0 320px">
|
||||||
<motion.div
|
<motion.div
|
||||||
custom={index}
|
custom={index}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
@@ -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'
|
||||||
}}>
|
}}>
|
||||||
|
{product.static_image_url ? (
|
||||||
|
<img
|
||||||
|
src={product.static_image_url}
|
||||||
|
alt={product.name}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ y: [0, -10, 0] }}
|
animate={{ y: [0, -10, 0] }}
|
||||||
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
|
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
|
||||||
>
|
>
|
||||||
<RocketOutlined style={{ fontSize: 60, color: '#00b96b' }} />
|
<RocketOutlined style={{ fontSize: 60, color: '#00b96b' }} />
|
||||||
</motion.div>
|
</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;
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user