316 lines
12 KiB
JavaScript
316 lines
12 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import { useParams, useLocation } from 'react-router-dom';
|
||
import axios from 'axios';
|
||
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);
|
||
const [imgOpen, setImgOpen] = useState(true);
|
||
const [highlightsOpen, setHighlightsOpen] = useState(false);
|
||
const [compareOpen, setCompareOpen] = useState(false);
|
||
const [promoOpen, setPromoOpen] = useState(false);
|
||
const [faqOpen, setFaqOpen] = useState(false);
|
||
const [reviewOpen, setReviewOpen] = 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(`/api/docs/images?${qs}`);
|
||
const data = await res.json();
|
||
if (data.success && Array.isArray(data.data) && data.data.length) {
|
||
setImages(data.data.map((d) => `${d.url}`));
|
||
}
|
||
} catch {}
|
||
};
|
||
loadImages();
|
||
}, [series, sub]);
|
||
|
||
useEffect(() => {
|
||
const loadSpecs = async () => {
|
||
try {
|
||
setSpecsLoading(true);
|
||
const qs = new URLSearchParams({ series, sub }).toString();
|
||
const res = await fetch(`/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(Object.fromEntries(data.data.map((_, i) => [i, false])));
|
||
} else {
|
||
setSections([{ title: '参数', rows: data.data }]);
|
||
setSpecs(data.data);
|
||
setExpanded({ 0: false });
|
||
}
|
||
}
|
||
} catch {}
|
||
finally {
|
||
setSpecsLoading(false);
|
||
}
|
||
};
|
||
loadSpecs();
|
||
}, [series, sub]);
|
||
|
||
useEffect(() => {
|
||
if (images.length <= 1) return;
|
||
const timer = setInterval(() => {
|
||
setActive((prev) => (prev + 1) % images.length);
|
||
}, 5000);
|
||
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="pd-title">{product.name}</div>
|
||
<div className="section-narrow">
|
||
<div className="pd-list">
|
||
<button className="pd-list-item" onClick={() => setImgOpen(!imgOpen)}>
|
||
<span>产品图片</span>
|
||
<span>{imgOpen ? 'v' : '›'}</span>
|
||
</button>
|
||
{imgOpen && (
|
||
<div className="pd-list-body">
|
||
{images.length > 1 ? (
|
||
<div className="size-carousel">
|
||
<div className="size-slides">
|
||
{images.map((src, idx) => (
|
||
<div key={idx} className={`size-slide ${idx === active ? 'active' : ''}`}>
|
||
<img className="size-slide-img" src={src} alt={`${product.name}-${idx+1}`} loading="lazy" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
<button className="size-arrow left" onClick={prevSlide}>‹</button>
|
||
<button className="size-arrow right" onClick={nextSlide}>›</button>
|
||
<div className="size-indicators">
|
||
{images.map((_, idx) => (
|
||
<span
|
||
key={idx}
|
||
className={`size-dot ${idx === active ? 'active' : ''}`}
|
||
onClick={() => setActive(idx)}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="image-row">
|
||
{(images.length ? images : (product?.image ? [product.image] : [])).map((src, idx) => (
|
||
<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>
|
||
);
|
||
};
|
||
|
||
const DocsList = ({ series, sub }) => {
|
||
const [docs, setDocs] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState('');
|
||
|
||
const handleDownload = async (d) => {
|
||
try {
|
||
const res = await axios.get(d.url, { responseType: 'blob' });
|
||
const blob = new Blob([res.data], { type: res.data.type || 'application/octet-stream' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = d.name || (String(d.url).split('/').pop() || '下载文件');
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
URL.revokeObjectURL(url);
|
||
} catch (e) {
|
||
setError('下载失败或文件不存在');
|
||
}
|
||
};
|
||
|
||
const handleOpen = async (d) => {
|
||
try {
|
||
const res = await axios.get(d.url, { responseType: 'blob' });
|
||
const blob = new Blob([res.data], { type: 'application/pdf' });
|
||
const url = URL.createObjectURL(blob);
|
||
window.open(url, '_blank');
|
||
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
||
} catch (e) {
|
||
window.open(d.url, '_blank');
|
||
}
|
||
};
|
||
|
||
React.useEffect(() => {
|
||
const fetchDocs = async () => {
|
||
try {
|
||
setLoading(true);
|
||
setError('');
|
||
const qs = new URLSearchParams({ series, sub }).toString();
|
||
const res = await fetch(`/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">
|
||
{String(d.url || '').toLowerCase().endsWith('.pdf') ? (
|
||
<a
|
||
href={`${d.url}`}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="docs-link"
|
||
onClick={(e) => { e.preventDefault(); handleOpen(d); }}
|
||
>
|
||
{d.name}
|
||
</a>
|
||
) : (
|
||
<a href={`${d.url}`} target="_blank" rel="noreferrer" className="docs-link">
|
||
{d.name}
|
||
</a>
|
||
)}
|
||
<span className="docs-size">{(d.size / 1024 / 1024).toFixed(2)}MB</span>
|
||
<button type="button" className="docs-download" onClick={() => handleDownload(d)}>下载</button>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
);
|
||
};
|
||
|
||
export default ProductDetail;
|