useState
管理條件、即時過濾 viewsrc/components/Projects.tsx
import React, { useState, useMemo } from 'react'
import type { Project } from '../data/projects'
type Props = {
items: Project[]
}
export default function Projects({ items }: Props) {
const [onlyFeatured, setOnlyFeatured] = useState(false)
const [keyword, setKeyword] = useState('')
// 過濾邏輯
const view = useMemo(() => {
const kw = keyword.toLowerCase()
return items.filter(p => {
const matchFeatured = !onlyFeatured || p.featured
const matchKeyword =
!kw ||
p.title.toLowerCase().includes(kw) ||
p.tech.toLowerCase().includes(kw) ||
p.desc.toLowerCase().includes(kw)
return matchFeatured && matchKeyword
})
}, [items, onlyFeatured, keyword])
return (
<section id="projects" className="container section">
<h2>作品集 Projects</h2>
<div style={{ margin: '12px 0', display: 'flex', gap: '12px', alignItems: 'center' }}>
<label>
<inputtype="checkbox"
checked={onlyFeatured}
onChange={(e) => setOnlyFeatured(e.target.checked)}
/>{' '}
只看精選
</label>
<inputtype="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="搜尋關鍵字"
style={{ flex: 1, maxWidth: '260px' }}
/>
<small className="muted">{view.length} / {items.length}</small>
</div>
<div className="project-grid">
{view.map((p) => (
<article className="card" key={p.id}>
<h3>{p.title}</h3>
<p className="muted">{p.tech}</p>
<p>{p.desc}</p>
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
{p.demo && <a className="btn small" href={p.demo} target="_blank">Demo</a>}
<a className="btn small btn-outline" href={p.repo} target="_blank">GitHub</a>
</div>
</article>
))}
</div>
</section>
)
}
featured: true
的項目.vue
元件開發體驗佳,資料綁定與響應式很直覺到這裡,三個框架我們都用來實作了 同一個履歷網站,這有幾個意義:
Props
,避免後續維護困難。恭喜!30 天你完成了: