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) => (
+
+

+
+ ))}
+
+
+ {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 (
+
+ {docs.map((d) => (
+ -
+
+ {d.name}
+
+ {(d.size / 1024 / 1024).toFixed(2)}MB
+ 下载
+
+ ))}
+
+ );
+};
+
+export default ProductDetail;