diff --git a/README.md b/README.md index 8d3f565..6ce2d95 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 机型信息管理系统 - 前端 +# 通信技术部 - 前端 ## 技术栈 - React 18 diff --git a/package.json b/package.json index 4b9f452..56709ad 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "department-web", "version": "1.0.0", - "description": "机型信息管理系统前端", + "description": "通信技术部前端", "private": true, "dependencies": { "react": "^18.2.0", diff --git a/public/index.html b/public/index.html index 5735961..5569377 100755 --- a/public/index.html +++ b/public/index.html @@ -4,8 +4,8 @@ - - 机型信息管理系统 + + 通信技术部 diff --git a/src/App.js b/src/App.js index 9b10bc7..8afe742 100755 --- a/src/App.js +++ b/src/App.js @@ -3,7 +3,8 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { AuthProvider } from './context/AuthContext'; import Login from './pages/Login'; import AircraftList from './pages/AircraftList'; -import AircraftDetail from './pages/AircraftDetail'; +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'; @@ -25,15 +26,16 @@ function App() { } /> - - - - } - /> - } /> + + + + } + /> + } /> + } /> diff --git a/src/components/Navbar.css b/src/components/Navbar.css index 3552b6f..6f70353 100755 --- a/src/components/Navbar.css +++ b/src/components/Navbar.css @@ -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; +} diff --git a/src/components/Navbar.js b/src/components/Navbar.js index c53a81f..b04d3b1 100755 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -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 = () => {
Logo -
- 产品 -
- 产品一 - 产品二 - 产品三 +
+ setProductsOpen((v) => !v)}>产品 +
+
+
系列
+
+ {productMenu.map((series) => ( + + ))} +
+
+
+
型号
+
+ {productMenu.find(s => s.key === activeSeries)?.subs.map((sub) => ( + + ))} +
+
+
+
介绍
+
+ {productMenu + .find(s => s.key === activeSeries) + ?.subs.find(sub => sub.key === activeSub) + ?.products.map((p, idx) => ( +
{ + setProductsOpen(false); + navigate(`/product/${activeSeries}/${activeSub}/${idx}`, { + state: { product: p, series: activeSeries, sub: activeSub } + }); + }} + style={{ cursor: 'pointer' }} + > + {p.name} +
+
{p.name}
+
{p.desc}
+
+
+ ))} +
+
diff --git a/src/pages/AircraftList.js b/src/pages/AircraftList.js index 93bead4..1f089d7 100755 --- a/src/pages/AircraftList.js +++ b/src/pages/AircraftList.js @@ -44,7 +44,7 @@ const AircraftList = () => { return (
-

机型信息管理系统

+

通信技术部

{ return (
-

机型信息管理系统

+

通信技术部

diff --git a/src/pages/ProductDetail.css b/src/pages/ProductDetail.css new file mode 100644 index 0000000..71467b8 --- /dev/null +++ b/src/pages/ProductDetail.css @@ -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; } diff --git a/src/pages/ProductDetail.js b/src/pages/ProductDetail.js new file mode 100644 index 0000000..f2d3d67 --- /dev/null +++ b/src/pages/ProductDetail.js @@ -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 ( +
+
+
未找到产品信息
+
+
+ ); + } + + const prevSlide = () => setActive((prev) => (prev - 1 + images.length) % images.length); + const nextSlide = () => setActive((prev) => (prev + 1) % images.length); + + return ( +
+
+
+
机型图片
+ {images.length > 1 && } + +
+ {images.map((src, idx) => ( +
+ {`${product.name}-${idx+1}`} +
+ ))} +
+ + {images.length > 1 && } + + {images.length > 1 && ( +
+ {images.map((_, idx) => ( + setActive(idx)} + /> + ))} +
+ )} +
+ +
+
+
详细参数
+ {specsLoading ? ( +
加载中...
+ ) : sections.length ? ( + sections.map((sec, i) => { + const isOpen = !!expanded[i]; + const toggle = () => setExpanded((prev) => ({ ...prev, [i]: !prev[i] })); + return ( +
+ + {isOpen && ( +
+ + + {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 ( + + + + + ); + }) + ) : ( + + )} + +
{cleanKey}{cleanVal}
暂无参数
+
+ )} +
+ ); + }) + ) : ( +
暂无参数
+ )} +
+
+
+
+
资料下载
+
+ +
+
+
+
+
+ ); +}; + +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
加载中...
; + if (error) return
{error}
; + + if (!docs.length) return
暂无资料
; + + return ( + + ); +}; + +export default ProductDetail;