修改主页,添加机型
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "department-web",
|
||||
"version": "1.0.0",
|
||||
"description": "机型信息管理系统前端",
|
||||
"description": "通信技术部前端",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="机型信息管理系统" />
|
||||
<title>机型信息管理系统</title>
|
||||
<meta name="description" content="通信技术部" />
|
||||
<title>通信技术部</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>您需要启用JavaScript才能运行此应用程序。</noscript>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AuthProvider } from './context/AuthContext';
|
||||
import Login from './pages/Login';
|
||||
import AircraftList from './pages/AircraftList';
|
||||
import AircraftDetail from './pages/AircraftDetail';
|
||||
import ProductDetail from './pages/ProductDetail';
|
||||
import PrivateRoute from './components/PrivateRoute';
|
||||
import Navbar from './components/Navbar';
|
||||
import Home from './pages/Home';
|
||||
@@ -33,6 +34,7 @@ function App() {
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/product/:series/:sub/:idx" element={<ProductDetail />} />
|
||||
<Route path="/" element={<Home />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
.root: {}
|
||||
:root {
|
||||
--navbar-height: 60px;
|
||||
}
|
||||
.navbar {
|
||||
background: white;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
min-height: var(--navbar-height);
|
||||
}
|
||||
|
||||
.navbar-inner {
|
||||
@@ -13,6 +18,8 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1002;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@@ -44,6 +51,14 @@
|
||||
.nav-item:hover .dropdown {
|
||||
display: block;
|
||||
}
|
||||
.nav-item:hover .dropdown.mega {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.nav-item.open .dropdown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: none;
|
||||
@@ -76,7 +91,9 @@
|
||||
}
|
||||
|
||||
.search-input {
|
||||
height: 36px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -90,3 +107,111 @@
|
||||
.username {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.navbar-right .btn {
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dropdown.mega {
|
||||
display: none;
|
||||
padding: 28px;
|
||||
width: min(95vw, 1400px);
|
||||
border-radius: 10px;
|
||||
position: fixed;
|
||||
top: var(--navbar-height);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1001;
|
||||
gap: 20px;
|
||||
background: #fff;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.nav-item.open .dropdown.mega,
|
||||
.nav-item:hover .dropdown.mega {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.mega-col {
|
||||
width: 320px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.mega-col:first-child {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.mega-col.mega-wide {
|
||||
flex: 1;
|
||||
min-width: 800px;
|
||||
}
|
||||
|
||||
.mega-title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.mega-products {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.mega-product {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mega-product img {
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.mega-product-info .name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mega-product-info .desc {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
.mega-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mega-item {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e8e8e8;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mega-item:hover {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.mega-item.active {
|
||||
background: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,133 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import './Navbar.css';
|
||||
|
||||
const productMenu = [
|
||||
{
|
||||
key: 'smart',
|
||||
label: '智能系列',
|
||||
subs: [
|
||||
{
|
||||
key: 'N96',
|
||||
label: 'N96',
|
||||
products: [
|
||||
{ name: 'N96', desc: 'Android11,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n96.png ' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'N96P',
|
||||
label: 'N96P',
|
||||
products: [
|
||||
{ name: 'N96P', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n96p.png' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'N92',
|
||||
label: 'N92',
|
||||
products: [
|
||||
{ name: 'N92', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n92.png' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'N86P',
|
||||
label: 'N86P',
|
||||
products: [
|
||||
{ name: 'N86P', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n86p.png' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'N86',
|
||||
label: 'N86',
|
||||
products: [
|
||||
{ name: 'N86', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n86.png' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'N82',
|
||||
label: 'N82',
|
||||
products: [
|
||||
{ name: 'N82', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n82.png' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'N80',
|
||||
label: 'N80',
|
||||
products: [
|
||||
{ name: 'N80', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n80.png' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'N62',
|
||||
label: 'N62',
|
||||
products: [
|
||||
{ name: 'N62', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n62.png' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'N6P',
|
||||
label: 'N6P',
|
||||
products: [
|
||||
{ name: 'N6P', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n6p.png' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'N6',
|
||||
label: 'N6',
|
||||
products: [
|
||||
{ name: 'N6', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n6.png' }
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'traditional',
|
||||
label: '传统系列',
|
||||
subs: [
|
||||
{
|
||||
key: 'k300',
|
||||
label: 'K300',
|
||||
products: [
|
||||
{ name: 'K300', desc: '标准版,覆盖主流需求', image: 'http://localhost:3001/uploads/menu/menu_k300.png' }
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'basic',
|
||||
label: '基础系列',
|
||||
subs: [
|
||||
{
|
||||
key: 'kd69',
|
||||
label: 'KD69',
|
||||
products: [
|
||||
{ name: 'KD69', desc: '专业场景,扩展性强', image: 'http://localhost:3001/uploads/menu/menu_kd69.png' },
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const Navbar = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, logout } = useAuth();
|
||||
const [search, setSearch] = useState('');
|
||||
const [activeSeries, setActiveSeries] = useState(productMenu[0].key);
|
||||
const [activeSub, setActiveSub] = useState(productMenu[0].subs[0].key);
|
||||
const productsRef = useRef(null);
|
||||
const [productsOpen, setProductsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e) => {
|
||||
if (productsRef.current && !productsRef.current.contains(e.target)) {
|
||||
setProductsOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleLoginClick = () => {
|
||||
navigate('/login');
|
||||
@@ -33,18 +154,73 @@ const Navbar = () => {
|
||||
<div className="navbar-left">
|
||||
<Link to="/" className="logo">
|
||||
<img
|
||||
src="http://localhost:3001/uploads/logo.svg"
|
||||
src="http://localhost:3001/uploads/menu/logo.svg"
|
||||
alt="Logo"
|
||||
className="logo-img"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<div className="nav-item">
|
||||
<span className="nav-label">产品</span>
|
||||
<div className="dropdown">
|
||||
<Link to="/" className="dropdown-item">产品一</Link>
|
||||
<Link to="/" className="dropdown-item">产品二</Link>
|
||||
<Link to="/" className="dropdown-item">产品三</Link>
|
||||
<div className={`nav-item ${productsOpen ? 'open' : ''}`} ref={productsRef}>
|
||||
<span className="nav-label" onClick={() => setProductsOpen((v) => !v)}>产品</span>
|
||||
<div className="dropdown mega">
|
||||
<div className="mega-col">
|
||||
<div className="mega-title">系列</div>
|
||||
<div className="mega-list">
|
||||
{productMenu.map((series) => (
|
||||
<button
|
||||
key={series.key}
|
||||
className={`mega-item ${series.key === activeSeries ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setActiveSeries(series.key);
|
||||
setActiveSub(series.subs[0]?.key || '');
|
||||
}}
|
||||
>
|
||||
{series.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mega-col">
|
||||
<div className="mega-title">型号</div>
|
||||
<div className="mega-list">
|
||||
{productMenu.find(s => s.key === activeSeries)?.subs.map((sub) => (
|
||||
<button
|
||||
key={sub.key}
|
||||
className={`mega-item ${sub.key === activeSub ? 'active' : ''}`}
|
||||
onClick={() => setActiveSub(sub.key)}
|
||||
>
|
||||
{sub.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mega-col mega-wide">
|
||||
<div className="mega-title">介绍</div>
|
||||
<div className="mega-products">
|
||||
{productMenu
|
||||
.find(s => s.key === activeSeries)
|
||||
?.subs.find(sub => sub.key === activeSub)
|
||||
?.products.map((p, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="mega-product"
|
||||
onClick={() => {
|
||||
setProductsOpen(false);
|
||||
navigate(`/product/${activeSeries}/${activeSub}/${idx}`, {
|
||||
state: { product: p, series: activeSeries, sub: activeSub }
|
||||
});
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<img src={p.image} alt={p.name} />
|
||||
<div className="mega-product-info">
|
||||
<div className="name">{p.name}</div>
|
||||
<div className="desc">{p.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ const AircraftList = () => {
|
||||
return (
|
||||
<div className="aircraft-list-page">
|
||||
<div className="container">
|
||||
<h1 style={{ margin: '0 0 16px', color: '#333' }}>机型信息管理系统</h1>
|
||||
<h1 style={{ margin: '0 0 16px', color: '#333' }}>通信技术部</h1>
|
||||
<div className="search-section">
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
:root {
|
||||
--home-carousel-height: 720px;
|
||||
--home-carousel-height: 860px;
|
||||
}
|
||||
|
||||
.home-page {
|
||||
|
||||
@@ -6,25 +6,25 @@ const slidesData = [
|
||||
id: 1,
|
||||
title: '行业资讯:新机型发布',
|
||||
description: '最新机型亮相航空展,性能全面升级',
|
||||
image: 'http://localhost:3001/uploads/home_1.png'
|
||||
image: 'http://localhost:3001/uploads/menu/home_1.png'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '解决方案:资料管理优化',
|
||||
description: '一体化资料管理平台上线,提效30%',
|
||||
image: 'http://localhost:3001/uploads/home_2.png'
|
||||
image: 'http://localhost:3001/uploads/menu/home_2.png'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '产品更新:PDF在线预览',
|
||||
description: '新增在线预览与多端适配功能',
|
||||
image: 'http://localhost:3001/uploads/home_3.png'
|
||||
image: 'http://localhost:3001/uploads/menu/home_3.png'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '客户案例:数字化转型',
|
||||
description: '多行业落地实践,推动数据驱动决策',
|
||||
image: 'http://localhost:3001/uploads/home_4.png'
|
||||
image: 'http://localhost:3001/uploads/menu/home_4.png'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const Login = () => {
|
||||
return (
|
||||
<div className="login-container">
|
||||
<div className="login-card">
|
||||
<h1 className="login-title">机型信息管理系统</h1>
|
||||
<h1 className="login-title">通信技术部</h1>
|
||||
<form onSubmit={handleSubmit} className="login-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">用户名</label>
|
||||
|
||||
43
src/pages/ProductDetail.css
Normal file
43
src/pages/ProductDetail.css
Normal file
@@ -0,0 +1,43 @@
|
||||
.product-detail-page { min-height: 100vh; background: #f5f5f5; }
|
||||
.product-detail-page .container { max-width: 100%; padding: 0; }
|
||||
.section-narrow { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
|
||||
.carousel-full { position: relative; background: #000; overflow: hidden; width: 100%; min-height: 560px; border-radius: 0; }
|
||||
.slides { display: grid; grid-template-columns: 100%; transform: translateX(0); }
|
||||
.slide { opacity: 0; transition: opacity 0.4s ease; display: none; }
|
||||
.slide.active { opacity: 1; display: block; }
|
||||
.slide-img { width: 100%; height: 720px; object-fit: cover; display: block; }
|
||||
.arrow { position: absolute; top: 50%; transform: translateY(-50%); background: rgba(255,255,255,0.85); border: 1px solid #e8e8e8; border-radius: 50%; width: 36px; height: 36px; cursor: pointer; }
|
||||
.arrow.left { left: 10px; }
|
||||
.arrow.right { right: 10px; }
|
||||
.indicators { display: flex; justify-content: center; gap: 8px; margin-top: 12px; }
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; background: #d9d9d9; cursor: pointer; }
|
||||
.dot.active { background: #1890ff; }
|
||||
.product-title { margin: 0; color: #333; font-size: 22px; font-weight: 600; }
|
||||
.product-desc { color: #666; }
|
||||
.specs-section { margin-top: 24px; }
|
||||
.specs-body { margin-top: 12px; background: #f6ffed; border: 1px solid #d9f7be; border-radius: 0; }
|
||||
.specs-group { margin: 12px 0; }
|
||||
.specs-group-header { width: 100%; text-align: left; padding: 10px 12px; border: 1px solid #d9f7be; border-radius: 4px; background: #f6ffed; cursor: pointer; font-weight: 600; }
|
||||
.specs-summary { padding: 12px 14px; border-bottom: 1px solid #f0f0f0; }
|
||||
.specs-table { width: 100%; border-collapse: collapse; border: 1px solid #d9d9d9; }
|
||||
.specs-table td { padding: 10px 14px; border: 1px solid #e8e8e8; }
|
||||
.specs-key { width: 180px; color: #666; }
|
||||
.specs-val { color: #333; }
|
||||
.error-state { text-align: center; padding: 60px 20px; color: #999; }
|
||||
.docs-section { margin-top: 16px; }
|
||||
.docs-list { list-style: none; padding: 0 8px 12px; margin: 0; }
|
||||
.docs-item { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.docs-link { color: #1890ff; text-decoration: none; }
|
||||
.docs-link:hover { text-decoration: underline; }
|
||||
.docs-size { color: #999; font-size: 12px; margin-left: auto; }
|
||||
.docs-download { color: #1890ff; text-decoration: none; }
|
||||
|
||||
.section-tag { display: inline-block; padding: 6px 10px; border-radius: 4px; font-size: 13px; font-weight: 600; margin: 12px 14px; }
|
||||
.tag-images { background: #e6f7ff; color: #1890ff; }
|
||||
.tag-specs { background: #f6ffed; color: #52c41a; }
|
||||
.tag-docs { background: #fff7e6; color: #fa8c16; }
|
||||
.carousel-full .section-tag { position: absolute; top: 12px; left: 12px; margin: 0; z-index: 2; }
|
||||
.specs-table tr:nth-child(even) { background: #fafafa; }
|
||||
.docs-item:hover { background: #fafafa; }
|
||||
|
||||
.docs-body { background: #fff7e6; border: 1px solid #ffd591; border-radius: 0; padding: 8px 8px; }
|
||||
228
src/pages/ProductDetail.js
Normal file
228
src/pages/ProductDetail.js
Normal file
@@ -0,0 +1,228 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import './ProductDetail.css';
|
||||
|
||||
const ProductDetail = () => {
|
||||
const { series, sub, idx } = useParams();
|
||||
const location = useLocation();
|
||||
const product = location.state?.product;
|
||||
const [images, setImages] = useState([]);
|
||||
const [active, setActive] = useState(0);
|
||||
const [specs, setSpecs] = useState([]);
|
||||
const [sections, setSections] = useState([]);
|
||||
const [expanded, setExpanded] = useState({});
|
||||
const [specsLoading, setSpecsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const srcs = product
|
||||
? (Array.isArray(product.images) && product.images.length
|
||||
? product.images
|
||||
: (product.image ? [product.image] : []))
|
||||
: [];
|
||||
setImages(srcs);
|
||||
setActive(0);
|
||||
}, [product]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const loadImages = async () => {
|
||||
try {
|
||||
const qs = new URLSearchParams({ series, sub }).toString();
|
||||
const res = await fetch(`http://localhost:3001/api/docs/images?${qs}`);
|
||||
const data = await res.json();
|
||||
if (data.success && Array.isArray(data.data) && data.data.length) {
|
||||
setImages(data.data.map((d) => `http://localhost:3001${d.url}`));
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
loadImages();
|
||||
}, [series, sub]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadSpecs = async () => {
|
||||
try {
|
||||
setSpecsLoading(true);
|
||||
const qs = new URLSearchParams({ series, sub }).toString();
|
||||
const res = await fetch(`http://localhost:3001/api/docs/specs?${qs}`);
|
||||
const data = await res.json();
|
||||
if (data.success && Array.isArray(data.data)) {
|
||||
if (data.data.length && typeof data.data[0]?.title === 'string' && Array.isArray(data.data[0]?.rows)) {
|
||||
setSections(data.data);
|
||||
setSpecs([]);
|
||||
setExpanded({});
|
||||
} else {
|
||||
setSections([{ title: '参数', rows: data.data }]);
|
||||
setSpecs(data.data);
|
||||
setExpanded({});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
finally {
|
||||
setSpecsLoading(false);
|
||||
}
|
||||
};
|
||||
loadSpecs();
|
||||
}, [series, sub]);
|
||||
|
||||
useEffect(() => {
|
||||
if (images.length <= 1) return;
|
||||
const timer = setInterval(() => {
|
||||
setActive((prev) => (prev + 1) % images.length);
|
||||
}, 4000);
|
||||
return () => clearInterval(timer);
|
||||
}, [images.length]);
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<div className="product-detail-page">
|
||||
<div className="container">
|
||||
<div className="error-state">未找到产品信息</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const prevSlide = () => setActive((prev) => (prev - 1 + images.length) % images.length);
|
||||
const nextSlide = () => setActive((prev) => (prev + 1) % images.length);
|
||||
|
||||
return (
|
||||
<div className="product-detail-page">
|
||||
<div className="container">
|
||||
<div className="carousel-full">
|
||||
<div className="section-tag tag-images">机型图片</div>
|
||||
{images.length > 1 && <button className="arrow left" onClick={prevSlide}>‹</button>}
|
||||
|
||||
<div className="slides">
|
||||
{images.map((src, idx) => (
|
||||
<div key={idx} className={`slide ${idx === active ? 'active' : ''}`}>
|
||||
<img className="slide-img" src={src} alt={`${product.name}-${idx+1}`} loading="lazy" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{images.length > 1 && <button className="arrow right" onClick={nextSlide}>›</button>}
|
||||
|
||||
{images.length > 1 && (
|
||||
<div className="indicators">
|
||||
{images.map((_, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={`dot ${idx === active ? 'active' : ''}`}
|
||||
onClick={() => setActive(idx)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="specs-section">
|
||||
<div className="section-narrow">
|
||||
<div className="section-tag tag-specs">详细参数</div>
|
||||
{specsLoading ? (
|
||||
<div className="specs-body"><table className="specs-table"><tbody><tr><td colSpan="2" className="specs-val">加载中...</td></tr></tbody></table></div>
|
||||
) : sections.length ? (
|
||||
sections.map((sec, i) => {
|
||||
const isOpen = !!expanded[i];
|
||||
const toggle = () => setExpanded((prev) => ({ ...prev, [i]: !prev[i] }));
|
||||
return (
|
||||
<div key={sec.title + i} className="specs-group">
|
||||
<button className="specs-group-header" onClick={toggle}>
|
||||
{sec.title} {isOpen ? '▲' : '▼'}
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="specs-body">
|
||||
<table className="specs-table">
|
||||
<tbody>
|
||||
{sec.rows.length ? (
|
||||
sec.rows.map((row) => {
|
||||
const k = String(row.key || '');
|
||||
const v = String(row.value || '');
|
||||
const kb = /^\*\*.*\*\*$/.test(k) || k.startsWith('!');
|
||||
const vb = /^\*\*.*\*\*$/.test(v) || v.startsWith('!');
|
||||
const bold = kb || vb;
|
||||
const cleanKey = k.replace(/^!/, '').replace(/^\*\*/, '').replace(/\*\*$/, '');
|
||||
const cleanVal = v.replace(/^!/, '').replace(/^\*\*/, '').replace(/\*\*$/, '');
|
||||
return (
|
||||
<tr key={row.key} className={bold ? 'specs-row-bold' : ''}>
|
||||
<td className={`specs-key ${bold ? 'specs-bold' : ''}`}>{cleanKey}</td>
|
||||
<td className={`specs-val ${bold ? 'specs-bold' : ''}`}>{cleanVal}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr><td className="specs-val" colSpan="2">暂无参数</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="specs-body"><table className="specs-table"><tbody><tr><td colSpan="2" className="specs-val">暂无参数</td></tr></tbody></table></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="docs-section">
|
||||
<div className="section-narrow">
|
||||
<div className="section-tag tag-docs">资料下载</div>
|
||||
<div className="docs-body">
|
||||
<DocsList series={series} sub={sub} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DocsList = ({ series, sub }) => {
|
||||
const [docs, setDocs] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchDocs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
const qs = new URLSearchParams({ series, sub }).toString();
|
||||
const res = await fetch(`http://localhost:3001/api/docs/list?${qs}`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setDocs(data.data || []);
|
||||
} else {
|
||||
setError('获取资料失败');
|
||||
}
|
||||
} catch (e) {
|
||||
setError('网络错误');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchDocs();
|
||||
}, [series, sub]);
|
||||
|
||||
if (loading) return <div className="docs-loading">加载中...</div>;
|
||||
if (error) return <div className="docs-error">{error}</div>;
|
||||
|
||||
if (!docs.length) return <div className="docs-empty">暂无资料</div>;
|
||||
|
||||
return (
|
||||
<ul className="docs-list">
|
||||
{docs.map((d) => (
|
||||
<li key={d.url} className="docs-item">
|
||||
<a href={`http://localhost:3001${d.url}`} target="_blank" rel="noreferrer" className="docs-link">
|
||||
{d.name}
|
||||
</a>
|
||||
<span className="docs-size">{(d.size / 1024 / 1024).toFixed(2)}MB</span>
|
||||
<a href={`http://localhost:3001${d.url}`} download className="docs-download">下载</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetail;
|
||||
Reference in New Issue
Block a user