airpredict/空气质量预测/源码/docs/superpowers/plans/2026-07-07-health-science-a...

475 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 健康装修科普可点击全文 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让 landing「健康装修科普」的 4 篇卡片可点击进入看原创全文,并提供 `/articles` 列表页与 `/article/:id` 详情页。
**Architecture:** 纯前端静态。新建单一内容模块 `src/common/articles.ts`4 篇原创全文 + `getArticle`landing 轮播、列表页、详情页三处共用。新增两条 hash 公开路由,无后端/数据库改动。
**Tech Stack:** UmiJS 4 + React 18 + Antd 5 + TypeScript。无单测框架 —— 用 `npx tsc --noEmit -p tsconfig.json` 做类型验证(既有仅 2 条 eslint 类型根告警属预存,新增文件须零错误),交互留浏览器实测。
## Global Constraints
- 工作目录:`C:/code/空气质量预测/源码/用户端/iapip-web`。所有相对路径以此为根。
- 页面为公开页,路由 `layout: false`,风格与 `src/pages/landing`、`src/pages/forum` 一致,内联样式即可,**不新增全局 CSS**。
- hash 路由:文章 `id` 必须为 URL 友好的 ASCII kebab-case slug。
- 文章 4 篇slug 固定:`gb-standards` / `formaldehyde-release` / `summer-exceed` / `predict-before-cma`
- 每篇正文结尾附免责声明段落:「本文为科普整理,预测结果仅供参考,以 CMA 检测为准。」
- 提交在仓库根 `C:/code``git add` 用含中文的完整路径),每任务末尾提交一次。
- 修改 `.umirc.ts` 后需重启前端 dev配置变更不热更新前端 dev 端口 8001。
---
## 文件结构
- `src/common/articles.ts`**新建**,文章类型 + 4 篇内容 + `getArticle`Task 1
- `src/pages/article/index.tsx`**新建**,详情页 `/article/:id`Task 2
- `src/pages/articles/index.tsx`**新建**,列表页 `/articles`Task 3
- `.umirc.ts` — 新增 2 条路由Task 2 加 `/article/:id`Task 3 加 `/articles`
- `src/pages/landing/index.tsx` — news 派生自 articles、卡片与「全部文章」接线Task 4
---
## Task 1: 文章内容模块 `src/common/articles.ts`
**Files:**
- Create: `src/common/articles.ts`
**Interfaces:**
- Produces: `interface ArticleSection { heading?: string; paragraphs: string[] }``interface Article { id: string; tag: string; date: string; title: string; summary: string; body: ArticleSection[]; sources?: string[] }``const articles: Article[]`4 篇);`function getArticle(id: string): Article | undefined`。
- [ ] **Step 1: 创建内容模块(完整内容如下,勿留占位)**
创建 `src/common/articles.ts`
```ts
export interface ArticleSection {
heading?: string
paragraphs: string[]
}
export interface Article {
id: string
tag: string
date: string
title: string
summary: string
body: ArticleSection[]
sources?: string[]
}
const DISCLAIMER = '本文为科普整理,预测结果仅供参考,以 CMA 检测为准。'
export const articles: Article[] = [
{
id: 'gb-standards',
tag: '政策解读',
date: '专栏 · 2026.05',
title: '两大国标怎么读GB/T 18883 与 GB 50325 的差别',
summary: '一个是“住进去后”的室内空气质量标准,一个是“交工验收时”的工程控制标准——限值与采样条件并不相同,看懂它们才能判断房子到底达不达标。',
body: [
{
heading: '两个标准,管的是两件不同的事',
paragraphs: [
'GB/T 18883《室内空气质量标准》管的是“人住进去之后”的日常居住环境属于推荐性标准编号带 GB/T衡量的是房间在正常生活状态下的空气质量。',
'GB 50325《民用建筑工程室内环境污染控制标准》管的是“工程交付验收时”的室内环境属于强制性工程标准是新建、扩建民用建筑竣工验收必须满足的底线。'
]
},
{
heading: '采样条件不同,数字不能直接比',
paragraphs: [
'两个标准最容易被忽略的差别是采样前的“关闭门窗时间”。工程验收类标准通常要求关闭门窗较短时间后采样,模拟的是交工状态;居住类标准要求关闭门窗较长时间后采样,模拟的是日常关窗睡觉的真实场景。',
'关闭时间越长,累积的污染物浓度往往越高。因此同一套房子,用两套标准、两种采样条件测出来的数字并不能直接比较,更不能混用限值。具体限值与采样时长以两部标准原文为准。'
]
},
{
heading: '该看哪一个',
paragraphs: [
'装修完工、准备验收,看 GB 50325已经入住、想给居住环境做“体检”看 GB/T 18883。',
'本系统在预测时同时对照两部国标的限值,帮助你在选材和验收两个阶段都心里有数。',
DISCLAIMER
]
}
],
sources: ['GB/T 18883 室内空气质量标准', 'GB 50325 民用建筑工程室内环境污染控制标准']
},
{
id: 'formaldehyde-release',
tag: '科普',
date: '专栏 · 2026.04',
title: '新装住宅的甲醛,为什么能持续释放 315 年?',
summary: '人造板里的脲醛树脂胶会缓慢分解释放甲醛,释放周期长、受温湿度影响大。短期通风只能降一时浓度,真正的关键在源头材料的选择。',
body: [
{
heading: '甲醛藏在哪里',
paragraphs: [
'室内甲醛的主要来源是人造板材——刨花板、密度板、胶合板等。它们用脲醛树脂胶把木屑、木片粘合成板,而脲醛树脂胶里既含有未完全反应的游离甲醛,也含有遇水遇热会缓慢分解的化学结构。'
]
},
{
heading: '为什么能释放这么久',
paragraphs: [
'脲醛树脂在温度和湿度作用下会发生缓慢水解,不断把结合态的甲醛释放为游离甲醛。这个过程非常缓慢却持续,板材类污染源的释放周期可长达数年,通常说的 315 年正来源于此。',
'相比之下,油漆、涂料等表面材料的挥发性污染物释放周期短得多(数周到数月),这也是为什么装修后长期超标的“主角”往往是板材而非油漆。'
]
},
{
heading: '通风为什么治标不治本',
paragraphs: [
'通风只是把已经释放到空气中的甲醛换走,降低当下的空气浓度,并不改变材料内部的释放源。一旦停止通风、关闭门窗,浓度又会回升,这就是常见的“反弹”。',
'真正有效的做法是从源头入手优先选用低释放等级E0/E1的板材、控制板材总用量再配合长期通风与温湿度管理。',
DISCLAIMER
]
}
],
sources: ['人造板脲醛树脂释放机理(通用建材科学知识)']
},
{
id: 'summer-exceed',
tag: '指南',
date: '专栏 · 2026.03',
title: '夏天为什么更容易超标?温度与释放速率',
summary: '温度升高,材料的甲醛释放速率会明显增大。这也是“冬天测达标、夏天又超标”的原因。预测时把环境温湿度纳入计算,结果才靠谱。',
body: [
{
heading: '温度:释放速率的放大器',
paragraphs: [
'温度越高,材料内部甲醛分子的扩散和释放越活跃,单位时间释放到空气中的量随之增大。夏季室温明显高于冬季,因此同一套房子在夏天测得的浓度往往更高。'
]
},
{
heading: '湿度也在帮倒忙',
paragraphs: [
'较高的空气湿度会加速脲醛树脂的水解,进一步促进甲醛释放。高温加上高湿的梅雨、盛夏季节,是甲醛浓度最容易冲高的时段。'
]
},
{
heading: '“冬测达标”不等于安全',
paragraphs: [
'冬季低温下测得达标,并不代表夏季高温高湿时依然达标。判断房子是否安全,必须结合采样时的温湿度条件来看。',
'本系统在预测中引入了温湿度修正因子,把房间实际的温度、湿度纳入计算,让预测结果更贴近你真正居住的季节。',
DISCLAIMER
]
}
],
sources: ['温湿度对材料释放速率的影响(通用建材科学知识)']
},
{
id: 'predict-before-cma',
tag: '方法',
date: '专栏 · 2026.02',
title: '先预测,再决定要不要做 CMA 检测',
summary: '上门检测有成本。用本系统先做一次免费预测、定位高风险房间与主要污染材料,再有针对性地安排第三方 CMA 检测,省钱也更有的放矢。',
body: [
{
heading: 'CMA 检测是什么',
paragraphs: [
'CMA中国计量认证检测是具有法律效力的第三方检测结果权威、可作为依据。但它需要在装修完成后上门采样存在时间与费用成本点位越多越贵。'
]
},
{
heading: '先预测的价值',
paragraphs: [
'在选材和装修阶段,用本系统先做一次免费预测:输入房间、环境与所用材料,估算各房间的污染物浓度,定位出高风险房间和贡献最大的污染材料。',
'这样你就能把有限的检测预算花在刀刃上——重点检测预测中接近或超过限值的房间,而不是每个房间都盲目下点位。'
]
},
{
heading: '怎么配合使用',
paragraphs: [
'预测显示某房间接近或超过限值,就针对该区域安排 CMA 检测复核;预测整体余量较大,则可适当减少检测点位。',
'预测基于材料释放模型与国标限值,是决策辅助手段;房子最终是否达标,仍以 CMA 检测结果为准。',
DISCLAIMER
]
}
],
sources: ['CMA 计量认证检测流程(通用知识)']
}
]
export function getArticle(id: string): Article | undefined {
return articles.find(a => a.id === id)
}
```
- [ ] **Step 2: 类型检查**
Run:
```bash
cd "C:/code/空气质量预测/源码/用户端/iapip-web" && npx tsc --noEmit -p tsconfig.json 2>&1 | grep -E "error TS" | grep -v "eslint" | grep -iE "articles" || echo "articles.ts 无类型错误"
```
Expected: `articles.ts 无类型错误`
- [ ] **Step 3: 确认 4 篇 slug 齐全**
Run:
```bash
grep -oE "id: '[a-z-]+'" "C:/code/空气质量预测/源码/用户端/iapip-web/src/common/articles.ts"
```
Expected: 4 行 —— `id: 'gb-standards'`、`id: 'formaldehyde-release'`、`id: 'summer-exceed'`、`id: 'predict-before-cma'`。
- [ ] **Step 4: Commit**
```bash
cd "C:/code" && git add "空气质量预测/源码/用户端/iapip-web/src/common/articles.ts" && git commit -m "feat(web): add health-science articles content module"
```
---
## Task 2: 文章详情页 `/article/:id`
**Files:**
- Create: `src/pages/article/index.tsx`
- Modify: `.umirc.ts`(新增 `/article/:id` 路由)
**Interfaces:**
- Consumes: `getArticle`、`Article` from `@/common/articles`
- Produces: 路由 `/article/:id`(组件 `./article`)。
- [ ] **Step 1: 创建详情页组件**
创建 `src/pages/article/index.tsx`
```tsx
import { history, useParams } from '@umijs/max'
import { getArticle } from '@/common/articles'
export default function ArticleDetail() {
const { id } = useParams<{ id: string }>()
const article = id ? getArticle(id) : undefined
if (!article) {
return (
<div style={{ maxWidth: 760, margin: '0 auto', padding: '80px 16px', textAlign: 'center' }}>
<p style={{ color: '#666' }}>文章不存在或已下线。</p>
<a onClick={() => history.push('/landing')} style={{ color: '#B43533' }}> 返回首页</a>
</div>
)
}
return (
<div style={{ maxWidth: 760, margin: '0 auto', padding: '32px 16px 64px' }}>
<a onClick={() => history.push('/landing')} style={{ color: '#888' }}> 返回首页</a>
<div style={{ marginTop: 20 }}>
<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>
<p style={{ color: '#666', fontSize: 15, margin: '8px 0 24px' }}>{article.summary}</p>
</div>
{article.body.map((sec, i) => (
<section key={i} style={{ marginBottom: 20 }}>
{sec.heading && <h2 style={{ fontSize: 18, margin: '18px 0 8px' }}>{sec.heading}</h2>}
{sec.paragraphs.map((p, j) => (
<p key={j} style={{ lineHeight: 1.9, color: '#333', margin: '0 0 10px' }}>{p}</p>
))}
</section>
))}
{article.sources && article.sources.length > 0 && (
<div style={{ marginTop: 28, paddingTop: 16, borderTop: '1px solid #eee', color: '#999', fontSize: 13 }}>
<div style={{ marginBottom: 6 }}>参考来源</div>
<ul style={{ margin: 0, paddingLeft: 18 }}>
{article.sources.map((s, i) => <li key={i}>{s}</li>)}
</ul>
</div>
)}
</div>
)
}
```
- [ ] **Step 2: 新增路由**
`.umirc.ts``routes` 数组中,`帖子详情``/forum/thread/:id`)路由项之后追加:
```ts
{
name: '文章详情',
path: '/article/:id',
component: './article',
layout: false
},
```
- [ ] **Step 3: 类型检查**
Run:
```bash
cd "C:/code/空气质量预测/源码/用户端/iapip-web" && npx tsc --noEmit -p tsconfig.json 2>&1 | grep -E "error TS" | grep -ivE "eslint" || echo "无新增类型错误"
```
Expected: `无新增类型错误`(仅可能残留既有的 2 条 eslint 告警,已被过滤)。
- [ ] **Step 4: 重启前端并浏览器验证**
重启前端 dev配置变更需重启停止当前 dev → 释放 8001 → `cd 用户端/iapip-web && PORT=8001 pnpm dev`
浏览器打开 `http://localhost:8001/iapip-web/#/article/gb-standards`
Expected: 显示该篇标题、`标签 · 日期`、摘要、分节正文(含小标题)、底部“参考来源”。访问 `#/article/nope` 显示“文章不存在”与返回链接。
- [ ] **Step 5: Commit**
```bash
cd "C:/code" && git add "空气质量预测/源码/用户端/iapip-web/src/pages/article/index.tsx" "空气质量预测/源码/用户端/iapip-web/.umirc.ts" && git commit -m "feat(web): article detail page and route"
```
---
## Task 3: 文章列表页 `/articles`
**Files:**
- Create: `src/pages/articles/index.tsx`
- Modify: `.umirc.ts`(新增 `/articles` 路由)
**Interfaces:**
- Consumes: `articles` from `@/common/articles`
- Produces: 路由 `/articles`(组件 `./articles`)。
- [ ] **Step 1: 创建列表页组件**
创建 `src/pages/articles/index.tsx`
```tsx
import { history } from '@umijs/max'
import { articles } from '@/common/articles'
export default function ArticleList() {
return (
<div style={{ maxWidth: 820, margin: '0 auto', padding: '32px 16px 64px' }}>
<a onClick={() => history.push('/landing')} style={{ color: '#888' }}> 返回首页</a>
<h1 style={{ fontSize: 24, margin: '16px 0 24px' }}>健康装修科普</h1>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{articles.map(a => (
<article
key={a.id}
onClick={() => history.push('/article/' + a.id)}
style={{ cursor: 'pointer', padding: 20, border: '1px solid #eee', borderRadius: 8, transition: 'box-shadow .2s' }}
>
<div style={{ color: '#B43533', fontSize: 13 }}>{a.tag} · {a.date}</div>
<h2 style={{ fontSize: 18, margin: '8px 0' }}>{a.title}</h2>
<p style={{ color: '#666', fontSize: 14, lineHeight: 1.7, margin: 0 }}>{a.summary}</p>
</article>
))}
</div>
</div>
)
}
```
- [ ] **Step 2: 新增路由**
`.umirc.ts``routes` 数组中Task 2 新增的 `文章详情` 路由项**之前**追加(`/articles` 与 `/article/:id` 路径前缀不同,顺序不影响匹配,但列表在前更清晰):
```ts
{
name: '科普文章',
path: '/articles',
component: './articles',
layout: false
},
```
- [ ] **Step 3: 类型检查**
Run:
```bash
cd "C:/code/空气质量预测/源码/用户端/iapip-web" && npx tsc --noEmit -p tsconfig.json 2>&1 | grep -E "error TS" | grep -ivE "eslint" || echo "无新增类型错误"
```
Expected: `无新增类型错误`
- [ ] **Step 4: 重启前端并浏览器验证**
重启前端 dev。浏览器打开 `http://localhost:8001/iapip-web/#/articles`
Expected: 列出 4 篇(标签/日期/标题/摘要),点击任一进入对应 `/article/:id` 详情。
- [ ] **Step 5: Commit**
```bash
cd "C:/code" && git add "空气质量预测/源码/用户端/iapip-web/src/pages/articles/index.tsx" "空气质量预测/源码/用户端/iapip-web/.umirc.ts" && git commit -m "feat(web): articles list page and route"
```
---
## Task 4: landing 接线(卡片可点、全部文章跳列表)
**Files:**
- Modify: `src/pages/landing/index.tsx`
**Interfaces:**
- Consumes: `articles` from `@/common/articles`(复用其 `id/tag/date/title/summary`)。
- [ ] **Step 1: 用 articles 替换本地 news 常量**
`landing/index.tsx` 顶部 import 段追加:
```ts
import { articles } from '@/common/articles'
```
删除文件中现有的本地 `const news = [ ... ]`4 条写死资讯的整段数组定义),并在其原位置替换为:
```ts
const news = articles
```
> 说明:`articles` 每项含 `tag/date/title/summary`,取代原 `news` 的 `tag/date/title/desc`。渲染处会把 `desc` 改为 `summary`(下一步)。
- [ ] **Step 2: 轮播卡片改用 summary 并加点击跳转**
`#news` 区块的轮播渲染中,找到 `news.map((n, i) => ( ... ))` 生成的 `<article className="cslide" key={i}>`。做两处改动:
其一,给整张卡加点击跳转——将 `<article className="cslide" key={i}>` 改为:
```tsx
<article className="cslide" key={i} style={{ cursor: 'pointer' }} onClick={() => history.push('/article/' + n.id)}>
```
其二,把卡片正文里的摘要字段 `n.desc` 改为 `n.summary`
```tsx
<p>{n.summary}</p>
```
`阅读全文` 的 `.cslide-more` 已在整卡点击范围内,无需单独绑定。)
- [ ] **Step 3: 「全部文章」按钮跳列表页**
`#news` 区块头部,将:
```tsx
<a className="btn" onClick={openPredict}>全部文章</a>
```
改为:
```tsx
<a className="btn" onClick={() => history.push('/articles')}>全部文章</a>
```
- [ ] **Step 4: 类型检查**
Run:
```bash
cd "C:/code/空气质量预测/源码/用户端/iapip-web" && npx tsc --noEmit -p tsconfig.json 2>&1 | grep -E "error TS" | grep -ivE "eslint" || echo "无新增类型错误"
```
Expected: `无新增类型错误`
- [ ] **Step 5: 浏览器验证landing 为 tsx热更新即可无需重启**
浏览器打开 `http://localhost:8001/iapip-web/#/landing`
Expected:
- 「健康装修科普」轮播卡片可点击 → 进入对应 `/article/:id` 全文。
- 顶部「全部文章」→ `/articles` 列表页。
- 轮播自动播放、左右切换、圆点交互不受影响;卡片摘要文字正常显示(非空白)。
- [ ] **Step 6: Commit**
```bash
cd "C:/code" && git add "空气质量预测/源码/用户端/iapip-web/src/pages/landing/index.tsx" && git commit -m "feat(web): wire landing news cards to article pages"
```
---
## Self-Review 记录
- **Spec 覆盖**内容模块→Task 1详情页 `/article/:id`→Task 2列表页 `/articles`→Task 3landing 接线(卡片可点 + 全部文章跳转 + news 派生自 articles→Task 4路由→Task 2/3。全覆盖。
- **类型一致**`Article`/`ArticleSection` 字段(`id/tag/date/title/summary/body/sources`、`heading/paragraphs`)在 Task 1 定义Task 2/3/4 消费一致;`getArticle(id)`、`articles` 命名一致landing 渲染由 `n.desc` 改为 `n.summary` 与新数据源匹配。
- **占位符**:无 TODO/TBD4 篇正文全文已写入 Task 1各步含具体命令与预期。
- **风险**landing 删除本地 `news` 数组时须整段删除、勿残留;`.umirc.ts` 改动后 Task 2/3 需重启前端Task 4 仅 tsx 热更新。