修改主页和机型信息展示页面

This commit is contained in:
huanglinhuan
2025-12-09 14:23:22 +08:00
parent 863fb26040
commit d50b08e33e
11 changed files with 237 additions and 803 deletions

View File

@@ -2,10 +2,7 @@ import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext'; import { AuthProvider } from './context/AuthContext';
import Login from './pages/Login'; import Login from './pages/Login';
import AircraftList from './pages/AircraftList'; import ProductDetail from './pages/ProductDetail';
import AircraftDetail from './pages/AircraftDetail';
import ProductDetail from './pages/ProductDetail';
import PrivateRoute from './components/PrivateRoute';
import Navbar from './components/Navbar'; import Navbar from './components/Navbar';
import Home from './pages/Home'; import Home from './pages/Home';
import './App.css'; import './App.css';
@@ -14,30 +11,13 @@ function App() {
return ( return (
<AuthProvider> <AuthProvider>
<Router> <Router>
<div className="App">
<Navbar /> <Navbar />
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route
path="/aircraft" <Route path="/product/:series/:sub/:idx" element={<ProductDetail />} />
element={ <Route path="/" element={<Home />} />
<PrivateRoute>
<AircraftList />
</PrivateRoute>
}
/>
<Route
path="/aircraft/:id"
element={
<PrivateRoute>
<AircraftDetail />
</PrivateRoute>
}
/>
<Route path="/product/:series/:sub/:idx" element={<ProductDetail />} />
<Route path="/" element={<Home />} />
</Routes> </Routes>
</div>
</Router> </Router>
</AuthProvider> </AuthProvider>
); );

View File

@@ -45,16 +45,15 @@
.nav-label { .nav-label {
color: #333; color: #333;
cursor: default; cursor: pointer;
font-weight: 700;
display: inline-block;
position: relative;
} }
.nav-item:hover .dropdown { .nav-item:hover .dropdown,
display: block;
}
.nav-item:hover .dropdown.mega { .nav-item:hover .dropdown.mega {
display: flex; display: none;
flex-wrap: nowrap;
align-items: flex-start;
} }
.nav-item.open .dropdown { .nav-item.open .dropdown {
display: block; display: block;
@@ -130,13 +129,23 @@
background: #fff; background: #fff;
pointer-events: auto; pointer-events: auto;
} }
.nav-item.open .dropdown.mega, .nav-item.open .dropdown.mega {
.nav-item:hover .dropdown.mega {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: flex-start; align-items: flex-start;
} }
.nav-item.selected .nav-label::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: -2px;
height: 2px;
background: #1890ff;
border-radius: 2px;
}
.mega-col { .mega-col {
width: 320px; width: 320px;
padding: 0 16px; padding: 0 16px;

View File

@@ -12,70 +12,70 @@ const productMenu = [
key: 'N96', key: 'N96',
label: 'N96', label: 'N96',
products: [ products: [
{ name: 'N96', desc: 'Android11高性能适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n96.png ' } { name: 'N96', desc: 'Android11高性能适配复杂场景', image: '/uploads/menu/menu_n96.png' }
] ]
}, },
{ {
key: 'N96P', key: 'N96P',
label: 'N96P', label: 'N96P',
products: [ products: [
{ name: 'N96P', desc: 'Android14高性能适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n96p.png' } { name: 'N96P', desc: 'Android14高性能适配复杂场景', image: '/uploads/menu/menu_n96p.png' }
] ]
}, },
{ {
key: 'N92', key: 'N92',
label: 'N92', label: 'N92',
products: [ products: [
{ name: 'N92', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n92.png' } { name: 'N92', desc: 'Android13,高性能,适配复杂场景', image: '/uploads/menu/menu_n92.png' }
] ]
}, },
{ {
key: 'N86P', key: 'N86P',
label: 'N86P', label: 'N86P',
products: [ products: [
{ name: 'N86P', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n86p.png' } { name: 'N86P', desc: 'Android13,高性能,适配复杂场景', image: '/uploads/menu/menu_n86p.png' }
] ]
}, },
{ {
key: 'N86', key: 'N86',
label: 'N86', label: 'N86',
products: [ products: [
{ name: 'N86', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n86.png' } { name: 'N86', desc: 'Android9,高性能,适配复杂场景', image: '/uploads/menu/menu_n86.png' }
] ]
}, },
{ {
key: 'N82', key: 'N82',
label: 'N82', label: 'N82',
products: [ products: [
{ name: 'N82', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n82.png' } { name: 'N82', desc: 'Android10,高性能,适配复杂场景', image: '/uploads/menu/menu_n82.png' }
] ]
}, },
{ {
key: 'N80', key: 'N80',
label: 'N80', label: 'N80',
products: [ products: [
{ name: 'N80', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n80.png' } { name: 'N80', desc: 'Android13,高性能,适配复杂场景', image: '/uploads/menu/menu_n80.png' }
] ]
}, },
{ {
key: 'N62', key: 'N62',
label: 'N62', label: 'N62',
products: [ products: [
{ name: 'N62', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n62.png' } { name: 'N62', desc: 'Android11,高性能,适配复杂场景', image: '/uploads/menu/menu_n62.png' }
] ]
}, },
{ {
key: 'N6P', key: 'N6P',
label: 'N6P', label: 'N6P',
products: [ products: [
{ name: 'N6P', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n6p.png' } { name: 'N6P', desc: 'Android13,高性能,适配复杂场景', image: '/uploads/menu/menu_n6p.png' }
] ]
}, },
{ {
key: 'N6', key: 'N6',
label: 'N6', label: 'N6',
products: [ products: [
{ name: 'N6', desc: 'Android14,高性能,适配复杂场景', image: 'http://localhost:3001/uploads/menu/menu_n6.png' } { name: 'N6', desc: 'Android7,高性能,适配复杂场景', image: '/uploads/menu/menu_n6.png' }
] ]
}, },
] ]
@@ -88,7 +88,7 @@ const productMenu = [
key: 'k300', key: 'k300',
label: 'K300', label: 'K300',
products: [ products: [
{ name: 'K300', desc: '标准版,覆盖主流需求', image: 'http://localhost:3001/uploads/menu/menu_k300.png' } { name: 'K300', desc: '标准版,覆盖主流需求', image: '/uploads/menu/menu_k300.png' }
] ]
}, },
] ]
@@ -101,7 +101,7 @@ const productMenu = [
key: 'kd69', key: 'kd69',
label: 'KD69', label: 'KD69',
products: [ products: [
{ name: 'KD69', desc: '专业场景,扩展性强', image: 'http://localhost:3001/uploads/menu/menu_kd69.png' }, { name: 'KD69', desc: '专业场景,扩展性强', image: '/uploads/menu/menu_kd69.png' },
] ]
}, },
] ]
@@ -116,11 +116,13 @@ const Navbar = () => {
const [activeSub, setActiveSub] = useState(productMenu[0].subs[0].key); const [activeSub, setActiveSub] = useState(productMenu[0].subs[0].key);
const productsRef = useRef(null); const productsRef = useRef(null);
const [productsOpen, setProductsOpen] = useState(false); const [productsOpen, setProductsOpen] = useState(false);
const [productsSelected, setProductsSelected] = useState(false);
useEffect(() => { useEffect(() => {
const handleClickOutside = (e) => { const handleClickOutside = (e) => {
if (productsRef.current && !productsRef.current.contains(e.target)) { if (productsRef.current && !productsRef.current.contains(e.target)) {
setProductsOpen(false); setProductsOpen(false);
setProductsSelected(false);
} }
}; };
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
@@ -140,11 +142,7 @@ const Navbar = () => {
const handleSearchKeyDown = (e) => { const handleSearchKeyDown = (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
if (user) { navigate('/');
navigate('/aircraft', { state: { searchTerm: search } });
} else {
navigate('/login');
}
} }
}; };
@@ -154,14 +152,23 @@ const Navbar = () => {
<div className="navbar-left"> <div className="navbar-left">
<Link to="/" className="logo"> <Link to="/" className="logo">
<img <img
src="http://localhost:3001/uploads/menu/logo.svg" src="/uploads/menu/logo.svg"
alt="Logo" alt="Logo"
className="logo-img" className="logo-img"
/> />
</Link> </Link>
<div className={`nav-item ${productsOpen ? 'open' : ''}`} ref={productsRef}> <div className={`nav-item ${productsOpen ? 'open' : ''} ${productsSelected ? 'selected' : ''}`} ref={productsRef}>
<span className="nav-label" onClick={() => setProductsOpen((v) => !v)}>产品</span> <span
className="nav-label"
onClick={() => {
setProductsOpen((prev) => {
const next = !prev;
setProductsSelected(next);
return next;
});
}}
>产品</span>
<div className="dropdown mega"> <div className="dropdown mega">
<div className="mega-col"> <div className="mega-col">
<div className="mega-title">系列</div> <div className="mega-title">系列</div>
@@ -224,23 +231,23 @@ const Navbar = () => {
</div> </div>
</div> </div>
<div className="nav-item"> {/* <div className="nav-item">
<span className="nav-label">解决方案</span> <span className="nav-label">解决方案</span>
<div className="dropdown"> <div className="dropdown">
<Link to="/" className="dropdown-item">解决方案A</Link> <Link to="/" className="dropdown-item">解决方案A</Link>
<Link to="/" className="dropdown-item">解决方案B</Link> <Link to="/" className="dropdown-item">解决方案B</Link>
<Link to="/" className="dropdown-item">解决方案C</Link> <Link to="/" className="dropdown-item">解决方案C</Link>
</div> </div>
</div> </div> */}
<div className="nav-item"> {/* <div className="nav-item">
<span className="nav-label">行业</span> <span className="nav-label">行业</span>
<div className="dropdown"> <div className="dropdown">
<Link to="/" className="dropdown-item">民航</Link> <Link to="/" className="dropdown-item">民航</Link>
<Link to="/" className="dropdown-item">通航</Link> <Link to="/" className="dropdown-item">通航</Link>
<Link to="/" className="dropdown-item">研发制造</Link> <Link to="/" className="dropdown-item">研发制造</Link>
</div> </div>
</div> </div> */}
</div> </div>
<div className="navbar-right"> <div className="navbar-right">

View File

@@ -1,221 +0,0 @@
.aircraft-detail-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.page-header {
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-content h1 {
margin: 0;
color: #333;
font-size: 24px;
}
.aircraft-detail-card {
background: white;
border-radius: 8px;
padding: 32px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.aircraft-header {
border-bottom: 2px solid #f0f0f0;
padding-bottom: 24px;
margin-bottom: 24px;
}
.aircraft-code-large {
display: inline-block;
background: #1890ff;
color: white;
padding: 6px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
}
.aircraft-name-large {
margin: 0 0 16px 0;
color: #333;
font-size: 32px;
font-weight: 600;
}
.aircraft-meta {
display: flex;
gap: 12px;
}
.manufacturer-badge,
.type-badge {
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
}
.manufacturer-badge {
background: #e6f7ff;
color: #1890ff;
}
.type-badge {
background: #f6ffed;
color: #52c41a;
}
.aircraft-description {
margin-bottom: 32px;
}
.aircraft-description h3,
.aircraft-specifications h3 {
margin: 0 0 16px 0;
color: #333;
font-size: 20px;
font-weight: 600;
}
.aircraft-description p {
color: #666;
line-height: 1.8;
font-size: 15px;
}
.aircraft-specifications {
margin-top: 32px;
}
.specs-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.spec-item {
display: flex;
flex-direction: column;
padding: 16px;
background: #fafafa;
border-radius: 6px;
}
.spec-label {
color: #999;
font-size: 12px;
margin-bottom: 8px;
text-transform: uppercase;
}
.spec-value {
color: #333;
font-size: 18px;
font-weight: 600;
}
.pdf-materials-section {
background: white;
border-radius: 8px;
padding: 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
}
.section-header h3 {
margin: 0;
color: #333;
font-size: 20px;
font-weight: 600;
}
.search-input-small {
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
min-width: 200px;
}
.search-input-small:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.pdf-materials-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.pdf-material-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #fafafa;
border-radius: 6px;
border: 1px solid #e8e8e8;
transition: all 0.3s;
}
.pdf-material-card:hover {
background: #f0f0f0;
border-color: #1890ff;
}
.pdf-material-info {
flex: 1;
}
.pdf-material-title {
margin: 0 0 8px 0;
color: #333;
font-size: 16px;
font-weight: 600;
}
.pdf-material-description {
margin: 0 0 12px 0;
color: #666;
font-size: 14px;
}
.pdf-material-meta {
display: flex;
gap: 20px;
color: #999;
font-size: 12px;
}
.loading,
.error-state,
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
font-size: 16px;
}

View File

@@ -1,186 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import axios from 'axios';
import './AircraftDetail.css';
const AircraftDetail = () => {
const { id } = useParams();
const navigate = useNavigate();
const { logout } = useAuth();
const [aircraft, setAircraft] = useState(null);
const [pdfMaterials, setPdfMaterials] = useState([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const fetchAircraftDetail = useCallback(async () => {
try {
setLoading(true);
const response = await axios.get(`/api/aircraft/${id}`);
if (response.data.success) {
setAircraft(response.data.data);
}
} catch (error) {
console.error('获取机型详情失败:', error);
if (error.response?.status === 401) {
logout();
navigate('/login');
}
} finally {
setLoading(false);
}
}, [id, logout, navigate]);
const fetchPdfMaterials = useCallback(async () => {
try {
const response = await axios.get(`/api/pdf/aircraft/${id}`, {
params: { search: searchTerm }
});
if (response.data.success) {
setPdfMaterials(response.data.data);
}
} catch (error) {
console.error('获取PDF资料失败:', error);
}
}, [id, searchTerm]);
useEffect(() => {
fetchAircraftDetail();
fetchPdfMaterials();
}, [fetchAircraftDetail, fetchPdfMaterials]);
useEffect(() => {
if (id) {
fetchPdfMaterials();
}
}, [id, searchTerm, fetchPdfMaterials]);
const handleViewPdf = async (materialId) => {
try {
const response = await axios.get(`/api/pdf/file/${materialId}`);
if (response.data.success) {
const fileUrl = `http://localhost:3001${response.data.data.fileUrl}`;
window.open(fileUrl, '_blank');
}
} catch (error) {
console.error('打开PDF失败:', error);
alert('无法打开PDF文件请确保文件存在');
}
};
if (loading) {
return (
<div className="aircraft-detail-page">
<div className="container">
<div className="loading">加载中...</div>
</div>
</div>
);
}
if (!aircraft) {
return (
<div className="aircraft-detail-page">
<div className="container">
<div className="error-state">机型信息不存在</div>
</div>
</div>
);
}
return (
<div className="aircraft-detail-page">
<div className="container">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<button onClick={() => navigate('/aircraft')} className="btn btn-secondary"> 返回列表</button>
<h1 style={{ margin: 0, color: '#333' }}>机型详情</h1>
<div></div>
</div>
<div className="aircraft-detail-card">
<div className="aircraft-header">
<div className="aircraft-code-large">{aircraft.code}</div>
<h2 className="aircraft-name-large">{aircraft.name}</h2>
<div className="aircraft-meta">
<span className="manufacturer-badge">{aircraft.manufacturer}</span>
<span className="type-badge">{aircraft.type}</span>
</div>
</div>
<div className="aircraft-description">
<h3>机型简介</h3>
<p>{aircraft.description}</p>
</div>
<div className="aircraft-specifications">
<h3>技术规格</h3>
<div className="specs-grid">
<div className="spec-item">
<span className="spec-label">最大载客量</span>
<span className="spec-value">{aircraft.specifications.maxPassengers} </span>
</div>
<div className="spec-item">
<span className="spec-label">最大航程</span>
<span className="spec-value">{aircraft.specifications.maxRange}</span>
</div>
<div className="spec-item">
<span className="spec-label">巡航速度</span>
<span className="spec-value">{aircraft.specifications.cruiseSpeed}</span>
</div>
<div className="spec-item">
<span className="spec-label">机长</span>
<span className="spec-value">{aircraft.specifications.length}</span>
</div>
<div className="spec-item">
<span className="spec-label">翼展</span>
<span className="spec-value">{aircraft.specifications.wingspan}</span>
</div>
</div>
</div>
</div>
<div className="pdf-materials-section">
<div className="section-header">
<h3>相关资料</h3>
<input
type="text"
placeholder="搜索资料..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input-small"
/>
</div>
{pdfMaterials.length === 0 ? (
<div className="empty-state">暂无相关资料</div>
) : (
<div className="pdf-materials-list">
{pdfMaterials.map((material) => (
<div key={material.id} className="pdf-material-card">
<div className="pdf-material-info">
<h4 className="pdf-material-title">{material.title}</h4>
<p className="pdf-material-description">{material.description}</p>
<div className="pdf-material-meta">
<span>上传日期: {material.uploadDate}</span>
<span>文件大小: {material.fileSize}</span>
</div>
</div>
<button
onClick={() => handleViewPdf(material.id)}
className="btn btn-primary"
>
查看PDF
</button>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
};
export default AircraftDetail;

View File

@@ -1,142 +0,0 @@
.aircraft-list-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.page-header {
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-content h1 {
margin: 0;
color: #333;
font-size: 24px;
}
.header-actions {
display: flex;
align-items: center;
gap: 15px;
}
.user-info {
color: #666;
font-size: 14px;
}
.search-section {
margin-bottom: 24px;
}
.search-input {
width: 100%;
max-width: 600px;
padding: 12px 16px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.search-input:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.loading {
text-align: center;
padding: 40px;
color: #999;
}
.aircraft-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.aircraft-card {
background: white;
border-radius: 8px;
padding: 24px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
border: 2px solid transparent;
}
.aircraft-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
border-color: #1890ff;
}
.aircraft-code {
display: inline-block;
background: #1890ff;
color: white;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-bottom: 12px;
}
.aircraft-name {
margin: 0 0 12px 0;
color: #333;
font-size: 20px;
font-weight: 600;
}
.aircraft-info {
display: flex;
flex-direction: column;
gap: 6px;
color: #666;
font-size: 14px;
}
.manufacturer {
font-weight: 500;
}
.type {
color: #999;
}
.card-arrow {
position: absolute;
right: 24px;
top: 50%;
transform: translateY(-50%);
font-size: 24px;
color: #d9d9d9;
transition: all 0.3s;
}
.aircraft-card:hover .card-arrow {
color: #1890ff;
transform: translateY(-50%) translateX(4px);
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
font-size: 16px;
}

View File

@@ -1,89 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import axios from 'axios';
import './AircraftList.css';
const AircraftList = () => {
const [aircrafts, setAircrafts] = useState([]);
const [loading, setLoading] = useState(true);
const location = useLocation();
const initialSearch = location.state?.searchTerm || '';
const [searchTerm, setSearchTerm] = useState(initialSearch);
const { logout } = useAuth();
const navigate = useNavigate();
const fetchAircrafts = useCallback(async () => {
try {
setLoading(true);
const response = await axios.get('/api/aircraft/list', {
params: { search: searchTerm }
});
if (response.data.success) {
setAircrafts(response.data.data);
}
} catch (error) {
console.error('获取机型列表失败:', error);
if (error.response?.status === 401) {
logout();
navigate('/login');
}
} finally {
setLoading(false);
}
}, [searchTerm, logout, navigate]);
useEffect(() => {
fetchAircrafts();
}, [fetchAircrafts]);
const handleAircraftClick = (id) => {
navigate(`/aircraft/${id}`);
};
return (
<div className="aircraft-list-page">
<div className="container">
<h1 style={{ margin: '0 0 16px', color: '#333' }}>通信技术部</h1>
<div className="search-section">
<input
type="text"
placeholder="搜索机型(名称、代码、制造商)..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
{loading ? (
<div className="loading">加载中...</div>
) : (
<div className="aircraft-grid">
{aircrafts.length === 0 ? (
<div className="empty-state">未找到机型信息</div>
) : (
aircrafts.map((aircraft) => (
<div
key={aircraft.id}
className="aircraft-card"
onClick={() => handleAircraftClick(aircraft.id)}
>
<div className="aircraft-code">{aircraft.code}</div>
<h3 className="aircraft-name">{aircraft.name}</h3>
<div className="aircraft-info">
<span className="manufacturer">{aircraft.manufacturer}</span>
<span className="type">{aircraft.type}</span>
</div>
<div className="card-arrow"></div>
</div>
))
)}
</div>
)}
</div>
</div>
);
};
export default AircraftList;

View File

@@ -5,26 +5,26 @@ const slidesData = [
{ {
id: 1, id: 1,
title: '行业资讯:新机型发布', title: '行业资讯:新机型发布',
description: '最新机型亮相航空展,性能全面升级', description: '最新机型亮相展,性能全面升级',
image: 'http://localhost:3001/uploads/menu/home_1.png' image: '/uploads/menu/home_1.jpg'
}, },
{ {
id: 2, id: 2,
title: '解决方案:资料管理优化', title: '解决方案:资料管理优化',
description: '一体化资料管理平台上线提效30%', description: '一体化资料管理平台上线提效30%',
image: 'http://localhost:3001/uploads/menu/home_2.png' image: '/uploads/menu/home_2.jpg'
}, },
{ {
id: 3, id: 3,
title: '产品更新PDF在线预览', title: '产品更新PDF在线预览',
description: '新增在线预览与多端适配功能', description: '新增在线预览与多端适配功能',
image: 'http://localhost:3001/uploads/menu/home_3.png' image: '/uploads/menu/home_3.jpg'
}, },
{ {
id: 4, id: 4,
title: '客户案例:数字化转型', title: '客户案例:数字化转型',
description: '多行业落地实践,推动数据驱动决策', description: '多行业落地实践,推动数据驱动决策',
image: 'http://localhost:3001/uploads/menu/home_4.png' image: '/uploads/menu/home_4.jpg'
} }
]; ];

View File

@@ -18,11 +18,11 @@ const Login = () => {
const result = await login(username, password); const result = await login(username, password);
if (result.success) { if (result.success) {
navigate('/aircraft'); navigate('/');
} else { } else {
setError(result.error); setError(result.error);
} }
setLoading(false); setLoading(false);
}; };

View File

@@ -15,14 +15,16 @@
.product-title { margin: 0; color: #333; font-size: 22px; font-weight: 600; } .product-title { margin: 0; color: #333; font-size: 22px; font-weight: 600; }
.product-desc { color: #666; } .product-desc { color: #666; }
.specs-section { margin-top: 24px; } .specs-section { margin-top: 24px; }
.specs-body { margin-top: 12px; background: #f6ffed; border: 1px solid #d9f7be; border-radius: 0; } .specs-body { margin-top: 0; background: #fff; border: none; border-radius: 0; padding: 0; }
.specs-group { margin: 12px 0; } .specs-group { margin: 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-group-header { width: 100%; text-align: left; padding: 18px 0; border: none; background: transparent; cursor: pointer; font-weight: 700; color: #111; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e8e8e8; }
.specs-summary { padding: 12px 14px; border-bottom: 1px solid #f0f0f0; } .specs-summary { padding: 12px 0; border-bottom: 1px solid #e8e8e8; }
.specs-table { width: 100%; border-collapse: collapse; border: 1px solid #d9d9d9; } .specs-table { width: 100%; border-collapse: collapse; border: none; background: #fff; }
.specs-table td { padding: 10px 14px; border: 1px solid #e8e8e8; } .specs-table tr { border-bottom: 1px solid #e8e8e8; }
.specs-key { width: 180px; color: #666; } .specs-table tr:last-child { border-bottom: none; }
.specs-val { color: #333; } .specs-table td { padding: 12px 14px; border: none; }
.specs-key { width: 220px; color: #666; text-align: left; }
.specs-val { color: #1890ff; text-align: center; }
.error-state { text-align: center; padding: 60px 20px; color: #999; } .error-state { text-align: center; padding: 60px 20px; color: #999; }
.docs-section { margin-top: 16px; } .docs-section { margin-top: 16px; }
.docs-list { list-style: none; padding: 0 8px 12px; margin: 0; } .docs-list { list-style: none; padding: 0 8px 12px; margin: 0; }
@@ -37,7 +39,36 @@
.tag-specs { background: #f6ffed; color: #52c41a; } .tag-specs { background: #f6ffed; color: #52c41a; }
.tag-docs { background: #fff7e6; color: #fa8c16; } .tag-docs { background: #fff7e6; color: #fa8c16; }
.carousel-full .section-tag { position: absolute; top: 12px; left: 12px; margin: 0; z-index: 2; } .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-item:hover { background: #fafafa; }
.docs-body { background: #fff7e6; border: 1px solid #ffd591; border-radius: 0; padding: 8px 8px; } .docs-body { background: #f5f5f5; border: none; border-radius: 0; padding: 8px 8px; }
.pd-title { text-align: center; font-size: 24px; font-weight: 700; color: #111; padding: 16px 0; }
.pd-panel { margin: 12px 0; }
.pd-panel { border: none; border-radius: 0; background: #fff; }
.pd-panel-header { width: 100%; text-align: left; padding: 12px 0; background: transparent; cursor: pointer; font-weight: 700; color: #111; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e8e8e8; }
.pd-panel-body { background: #fff; padding: 12px 0; }
.pd-panel.expanded { border: 1px dashed #bfbfbf; border-radius: 6px; }
.pd-panel.expanded .pd-panel-header { padding: 12px 14px; border-bottom: none; }
.image-row { display: flex; justify-content: center; align-items: center; gap: 40px; padding: 12px; }
.image-row img { height: 420px; width: auto; object-fit: contain; display: block; }
.size-carousel { position: relative; background: #fff; overflow: hidden; width: 100%; min-height: 420px; border-radius: 0; }
.size-slides { display: grid; grid-template-columns: 100%; }
.size-slide { opacity: 0; transition: opacity 0.3s ease; display: none; }
.size-slide.active { opacity: 1; display: block; }
.size-slide-img { width: 100%; height: 420px; object-fit: contain; display: block; }
.size-arrow { position: absolute; top: 50%; transform: translateY(-50%); background: rgba(255,255,255,0.85); border: 1px solid #e8e8e8; border-radius: 50%; width: 32px; height: 32px; cursor: pointer; }
.size-arrow.left { left: 8px; }
.size-arrow.right { right: 8px; }
.size-indicators { display: flex; justify-content: center; gap: 8px; margin-top: 8px; }
.size-dot { width: 8px; height: 8px; border-radius: 50%; background: #d9d9d9; cursor: pointer; }
.size-dot.active { background: #1890ff; }
.device-figure { position: relative; display: flex; align-items: center; justify-content: center; }
.device-title { position: absolute; top: -26px; left: 50%; transform: translateX(-50%); font-weight: 700; color: #111; }
.diag-line { position: absolute; left: 10%; top: 10%; width: 80%; height: 0; border-top: 2px solid rgba(255,255,255,0.9); transform: rotate(-35deg); }
.diag-label { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); color: #fff; font-weight: 700; text-shadow: 0 1px 2px rgba(0,0,0,0.6); }
.device-figure.stylus .diag-line, .device-figure.stylus .diag-label, .device-figure.stylus .device-title { display: none; }
.pd-list { margin-top: 24px; }
.pd-list-item { width: 100%; text-align: left; background: transparent; border: none; border-bottom: 1px solid #e8e8e8; padding: 18px 0; font-weight: 700; color: #111; display: flex; justify-content: space-between; align-items: center; }
.pd-list-body { padding: 12px 0; }
.specs-sublist { padding-left: 28px; }

View File

@@ -12,6 +12,12 @@ const ProductDetail = () => {
const [sections, setSections] = useState([]); const [sections, setSections] = useState([]);
const [expanded, setExpanded] = useState({}); const [expanded, setExpanded] = useState({});
const [specsLoading, setSpecsLoading] = useState(false); const [specsLoading, setSpecsLoading] = useState(false);
const [imgOpen, setImgOpen] = useState(true);
const [highlightsOpen, setHighlightsOpen] = useState(false);
const [compareOpen, setCompareOpen] = useState(true);
const [promoOpen, setPromoOpen] = useState(false);
const [faqOpen, setFaqOpen] = useState(false);
const [reviewOpen, setReviewOpen] = useState(false);
useEffect(() => { useEffect(() => {
const srcs = product const srcs = product
@@ -29,10 +35,10 @@ const ProductDetail = () => {
const loadImages = async () => { const loadImages = async () => {
try { try {
const qs = new URLSearchParams({ series, sub }).toString(); const qs = new URLSearchParams({ series, sub }).toString();
const res = await fetch(`http://localhost:3001/api/docs/images?${qs}`); const res = await fetch(`/api/docs/images?${qs}`);
const data = await res.json(); const data = await res.json();
if (data.success && Array.isArray(data.data) && data.data.length) { if (data.success && Array.isArray(data.data) && data.data.length) {
setImages(data.data.map((d) => `http://localhost:3001${d.url}`)); setImages(data.data.map((d) => `${d.url}`));
} }
} catch {} } catch {}
}; };
@@ -44,17 +50,17 @@ const ProductDetail = () => {
try { try {
setSpecsLoading(true); setSpecsLoading(true);
const qs = new URLSearchParams({ series, sub }).toString(); const qs = new URLSearchParams({ series, sub }).toString();
const res = await fetch(`http://localhost:3001/api/docs/specs?${qs}`); const res = await fetch(`/api/docs/specs?${qs}`);
const data = await res.json(); const data = await res.json();
if (data.success && Array.isArray(data.data)) { if (data.success && Array.isArray(data.data)) {
if (data.data.length && typeof data.data[0]?.title === 'string' && Array.isArray(data.data[0]?.rows)) { if (data.data.length && typeof data.data[0]?.title === 'string' && Array.isArray(data.data[0]?.rows)) {
setSections(data.data); setSections(data.data);
setSpecs([]); setSpecs([]);
setExpanded({}); setExpanded(Object.fromEntries(data.data.map((_, i) => [i, true])));
} else { } else {
setSections([{ title: '参数', rows: data.data }]); setSections([{ title: '参数', rows: data.data }]);
setSpecs(data.data); setSpecs(data.data);
setExpanded({}); setExpanded({ 0: true });
} }
} }
} catch {} } catch {}
@@ -89,90 +95,129 @@ const ProductDetail = () => {
return ( return (
<div className="product-detail-page"> <div className="product-detail-page">
<div className="container"> <div className="container">
<div className="carousel-full"> <div className="pd-title">{product.name}{product.desc ? ` | ${product.desc}` : ''}</div>
<div className="section-tag tag-images">机型图片</div> <div className="section-narrow">
{images.length > 1 && <button className="arrow left" onClick={prevSlide}></button>} <div className="pd-list">
<button className="pd-list-item" onClick={() => setImgOpen(!imgOpen)}>
<div className="slides"> <span>产品图片</span>
{images.map((src, idx) => ( <span>{imgOpen ? 'v' : ''}</span>
<div key={idx} className={`slide ${idx === active ? 'active' : ''}`}> </button>
<img className="slide-img" src={src} alt={`${product.name}-${idx+1}`} loading="lazy" /> {imgOpen && (
</div> <div className="pd-list-body">
))} {images.length > 1 ? (
</div> <div className="size-carousel">
<div className="size-slides">
{images.length > 1 && <button className="arrow right" onClick={nextSlide}></button>} {images.map((src, idx) => (
<div key={idx} className={`size-slide ${idx === active ? 'active' : ''}`}>
{images.length > 1 && ( <img className="size-slide-img" src={src} alt={`${product.name}-${idx+1}`} loading="lazy" />
<div className="indicators"> </div>
{images.map((_, idx) => ( ))}
<span </div>
key={idx} <button className="size-arrow left" onClick={prevSlide}></button>
className={`dot ${idx === active ? 'active' : ''}`} <button className="size-arrow right" onClick={nextSlide}></button>
onClick={() => setActive(idx)} <div className="size-indicators">
/> {images.map((_, idx) => (
))} <span
</div> key={idx}
)} className={`size-dot ${idx === active ? 'active' : ''}`}
</div> onClick={() => setActive(idx)}
/>
<div className="specs-section"> ))}
<div className="section-narrow"> </div>
<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>
); ) : (
}) <div className="image-row">
) : ( {(images.length ? images : (product?.image ? [product.image] : [])).map((src, idx) => (
<div className="specs-body"><table className="specs-table"><tbody><tr><td colSpan="2" className="specs-val">暂无参数</td></tr></tbody></table></div> <img key={idx} src={src} alt={`${product.name}-${idx+1}`} loading="lazy" />
))}
</div>
)}
</div>
)}
<button className="pd-list-item" onClick={() => setHighlightsOpen(!highlightsOpen)}>
<span>通信功能</span>
<span>{highlightsOpen ? 'v' : ''}</span>
</button>
{highlightsOpen && <div className="pd-list-body"></div>}
<button className="pd-list-item" onClick={() => setCompareOpen(!compareOpen)}>
<span>详细参数</span>
<span>{compareOpen ? 'v' : ''}</span>
</button>
{compareOpen && (
<div className="pd-list-body">
<div className="specs-sublist">
{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 ? 'v' : ''}
</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>
)}
{/* <button className="pd-list-item" onClick={() => setPromoOpen(!promoOpen)}>
<span>活动说明</span>
<span>{promoOpen ? 'v' : ''}</span>
</button> */}
{promoOpen && <div className="pd-list-body"></div>}
<button className="pd-list-item" onClick={() => setFaqOpen(!faqOpen)}>
<span>常见问题</span>
<span>{faqOpen ? 'v' : ''}</span>
</button>
{faqOpen && <div className="pd-list-body"></div>}
<button className="pd-list-item" onClick={() => setReviewOpen(!reviewOpen)}>
<span>资料下载</span>
<span>{reviewOpen ? 'v' : ''}</span>
</button>
{reviewOpen && (
<div className="pd-list-body">
<div className="docs-body">
<DocsList series={series} sub={sub} />
</div>
</div>
)} )}
</div> </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>
</div> </div>
); );
@@ -189,7 +234,7 @@ const DocsList = ({ series, sub }) => {
setLoading(true); setLoading(true);
setError(''); setError('');
const qs = new URLSearchParams({ series, sub }).toString(); const qs = new URLSearchParams({ series, sub }).toString();
const res = await fetch(`http://localhost:3001/api/docs/list?${qs}`); const res = await fetch(`/api/docs/list?${qs}`);
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
setDocs(data.data || []); setDocs(data.data || []);
@@ -214,11 +259,11 @@ const DocsList = ({ series, sub }) => {
<ul className="docs-list"> <ul className="docs-list">
{docs.map((d) => ( {docs.map((d) => (
<li key={d.url} className="docs-item"> <li key={d.url} className="docs-item">
<a href={`http://localhost:3001${d.url}`} target="_blank" rel="noreferrer" className="docs-link"> <a href={`${d.url}`} target="_blank" rel="noreferrer" className="docs-link">
{d.name} {d.name}
</a> </a>
<span className="docs-size">{(d.size / 1024 / 1024).toFixed(2)}MB</span> <span className="docs-size">{(d.size / 1024 / 1024).toFixed(2)}MB</span>
<a href={`http://localhost:3001${d.url}`} download className="docs-download">下载</a> <a href={`${d.url}`} download className="docs-download">下载</a>
</li> </li>
))} ))}
</ul> </ul>