feat(web): 科普文章配真实免版权配图

4 张 Unsplash 免版权图自托管于 public/images/articles/,接入
资讯卡片、文章详情头图与列表缩略图。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7EkGJYZMdQtMeDEt3z4vL
This commit is contained in:
zty 2026-07-07 04:30:07 -04:00
parent a8d8c02a44
commit cae6779bcf
8 changed files with 18 additions and 6 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -9,10 +9,14 @@ export interface Article {
date: string date: string
title: string title: string
summary: string summary: string
image: string
body: ArticleSection[] body: ArticleSection[]
sources?: string[] sources?: string[]
} }
// 配图统一放在 public/images/articles/<id>.jpgpublicPath 为 /iapip-web/
const img = (id: string) => `/iapip-web/images/articles/${id}.jpg`
const DISCLAIMER = '本文为科普整理,预测结果仅供参考,以 CMA 检测为准。' const DISCLAIMER = '本文为科普整理,预测结果仅供参考,以 CMA 检测为准。'
export const articles: Article[] = [ export const articles: Article[] = [
@ -22,6 +26,7 @@ export const articles: Article[] = [
date: '专栏 · 2026.05', date: '专栏 · 2026.05',
title: '两大国标怎么读GB/T 18883 与 GB 50325 的差别', title: '两大国标怎么读GB/T 18883 与 GB 50325 的差别',
summary: '一个是“住进去后”的室内空气质量标准,一个是“交工验收时”的工程控制标准——限值与采样条件并不相同,看懂它们才能判断房子到底达不达标。', summary: '一个是“住进去后”的室内空气质量标准,一个是“交工验收时”的工程控制标准——限值与采样条件并不相同,看懂它们才能判断房子到底达不达标。',
image: img('gb-standards'),
body: [ body: [
{ {
heading: '两个标准,管的是两件不同的事', heading: '两个标准,管的是两件不同的事',
@ -54,6 +59,7 @@ export const articles: Article[] = [
date: '专栏 · 2026.04', date: '专栏 · 2026.04',
title: '新装住宅的甲醛,为什么能持续释放 315 年?', title: '新装住宅的甲醛,为什么能持续释放 315 年?',
summary: '人造板里的脲醛树脂胶会缓慢分解释放甲醛,释放周期长、受温湿度影响大。短期通风只能降一时浓度,真正的关键在源头材料的选择。', summary: '人造板里的脲醛树脂胶会缓慢分解释放甲醛,释放周期长、受温湿度影响大。短期通风只能降一时浓度,真正的关键在源头材料的选择。',
image: img('formaldehyde-release'),
body: [ body: [
{ {
heading: '甲醛藏在哪里', heading: '甲醛藏在哪里',
@ -85,6 +91,7 @@ export const articles: Article[] = [
date: '专栏 · 2026.03', date: '专栏 · 2026.03',
title: '夏天为什么更容易超标?温度与释放速率', title: '夏天为什么更容易超标?温度与释放速率',
summary: '温度升高,材料的甲醛释放速率会明显增大。这也是“冬天测达标、夏天又超标”的原因。预测时把环境温湿度纳入计算,结果才靠谱。', summary: '温度升高,材料的甲醛释放速率会明显增大。这也是“冬天测达标、夏天又超标”的原因。预测时把环境温湿度纳入计算,结果才靠谱。',
image: img('summer-exceed'),
body: [ body: [
{ {
heading: '温度:释放速率的放大器', heading: '温度:释放速率的放大器',
@ -115,6 +122,7 @@ export const articles: Article[] = [
date: '专栏 · 2026.02', date: '专栏 · 2026.02',
title: '先预测,再决定要不要做 CMA 检测', title: '先预测,再决定要不要做 CMA 检测',
summary: '上门检测有成本。用本系统先做一次免费预测、定位高风险房间与主要污染材料,再有针对性地安排第三方 CMA 检测,省钱也更有的放矢。', summary: '上门检测有成本。用本系统先做一次免费预测、定位高风险房间与主要污染材料,再有针对性地安排第三方 CMA 检测,省钱也更有的放矢。',
image: img('predict-before-cma'),
body: [ body: [
{ {
heading: 'CMA 检测是什么', heading: 'CMA 检测是什么',

View File

@ -20,7 +20,8 @@ export default function ArticleDetail() {
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<div style={{ color: '#B43533', fontSize: 13 }}>{article.tag} · {article.date}</div> <div style={{ color: '#B43533', fontSize: 13 }}>{article.tag} · {article.date}</div>
<h1 style={{ fontSize: 26, margin: '10px 0 4px', lineHeight: 1.35 }}>{article.title}</h1> <h1 style={{ fontSize: 26, margin: '10px 0 4px', lineHeight: 1.35 }}>{article.title}</h1>
<p style={{ color: '#666', fontSize: 15, margin: '8px 0 24px' }}>{article.summary}</p> <p style={{ color: '#666', fontSize: 15, margin: '8px 0 20px' }}>{article.summary}</p>
<img src={article.image} alt={article.title} style={{ width: '100%', height: 320, objectFit: 'cover', borderRadius: 12, display: 'block', marginBottom: 8 }} />
</div> </div>
{article.body.map((sec, i) => ( {article.body.map((sec, i) => (
<section key={i} style={{ marginBottom: 20 }}> <section key={i} style={{ marginBottom: 20 }}>

View File

@ -11,11 +11,14 @@ export default function ArticleList() {
<article <article
key={a.id} key={a.id}
onClick={() => history.push('/article/' + a.id)} onClick={() => history.push('/article/' + a.id)}
style={{ cursor: 'pointer', padding: 20, border: '1px solid #eee', borderRadius: 8, transition: 'box-shadow .2s' }} style={{ cursor: 'pointer', display: 'flex', gap: 16, padding: 16, border: '1px solid #eee', borderRadius: 8, transition: 'box-shadow .2s' }}
> >
<img src={a.image} alt={a.title} style={{ width: 160, height: 110, objectFit: 'cover', borderRadius: 6, flexShrink: 0 }} />
<div style={{ minWidth: 0 }}>
<div style={{ color: '#B43533', fontSize: 13 }}>{a.tag} · {a.date}</div> <div style={{ color: '#B43533', fontSize: 13 }}>{a.tag} · {a.date}</div>
<h2 style={{ fontSize: 18, margin: '8px 0' }}>{a.title}</h2> <h2 style={{ fontSize: 18, margin: '6px 0 8px' }}>{a.title}</h2>
<p style={{ color: '#666', fontSize: 14, lineHeight: 1.7, margin: 0 }}>{a.summary}</p> <p style={{ color: '#666', fontSize: 14, lineHeight: 1.7, margin: 0 }}>{a.summary}</p>
</div>
</article> </article>
))} ))}
</div> </div>

View File

@ -126,7 +126,7 @@ export default function Landing() {
<div className="ctrack" style={{ transform: `translateX(${-cur * 100}%)` }}> <div className="ctrack" style={{ transform: `translateX(${-cur * 100}%)` }}>
{news.map((n, i) => ( {news.map((n, i) => (
<article className="cslide" key={i} style={{ cursor: 'pointer' }} onClick={() => history.push('/article/' + n.id)}> <article className="cslide" key={i} style={{ cursor: 'pointer' }} onClick={() => history.push('/article/' + n.id)}>
<div className="cslide-img"><span className="cslide-tag">{n.tag}</span><div className="img-ph"></div></div> <div className="cslide-img"><span className="cslide-tag">{n.tag}</span><img src={n.image} alt={n.title} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} /></div>
<div className="cslide-body"> <div className="cslide-body">
<div className="cslide-date">{n.date}</div> <div className="cslide-date">{n.date}</div>
<h3>{n.title}</h3> <h3>{n.title}</h3>