Project_TeamDirectory.jsx
React.js/lessons/04_props/projects/Project_TeamDirectory.jsx
/**
* Project: 團隊通訊錄 — Props 練習
*
* 學習重點:
* - Props 傳遞各種類型(字串、數字、陣列、物件、函式、布林值)
* - Props 解構與預設值
* - children prop
* - 從子元件傳遞資訊給父元件(callback props)
* - 展開運算子傳遞 props
*
* 使用方式:將此檔案內容複製到你的 App.jsx 中
*/
import { useState } from 'react';
// ============================================================
// 資料
// ============================================================
const teamMembers = [
{
id: 1,
name: 'Alice Chen',
role: 'Frontend Lead',
department: 'Engineering',
email: 'alice@company.com',
phone: '0912-111-222',
skills: ['React', 'TypeScript', 'CSS'],
status: 'online',
avatar: '👩💻',
},
{
id: 2,
name: 'Bob Wang',
role: 'Backend Developer',
department: 'Engineering',
email: 'bob@company.com',
phone: '0912-333-444',
skills: ['Node.js', 'Python', 'PostgreSQL'],
status: 'busy',
avatar: '👨💻',
},
{
id: 3,
name: 'Carol Lin',
role: 'UI/UX Designer',
department: 'Design',
email: 'carol@company.com',
phone: '0912-555-666',
skills: ['Figma', 'Sketch', 'User Research'],
status: 'offline',
avatar: '👩🎨',
},
{
id: 4,
name: 'David Lee',
role: 'Product Manager',
department: 'Product',
email: 'david@company.com',
phone: '0912-777-888',
skills: ['Scrum', 'Data Analysis', 'Strategy'],
status: 'online',
avatar: '👨💼',
},
{
id: 5,
name: 'Eva Huang',
role: 'DevOps Engineer',
department: 'Engineering',
email: 'eva@company.com',
phone: '0912-999-000',
skills: ['Docker', 'K8s', 'AWS', 'CI/CD'],
status: 'online',
avatar: '👩🔧',
},
];
// ============================================================
// 元件:Card 容器(使用 children prop)
// ============================================================
const Card = ({ children, highlighted = false, onClick }) => (
<div
onClick={onClick}
style={{
padding: '20px',
borderRadius: '12px',
border: `2px solid ${highlighted ? '#818cf8' : '#e2e8f0'}`,
backgroundColor: highlighted ? '#eef2ff' : 'white',
cursor: onClick ? 'pointer' : 'default',
transition: 'all 0.2s',
marginBottom: '12px',
}}
>
{children}
</div>
);
// ============================================================
// 元件:狀態指示燈
// ============================================================
const StatusIndicator = ({ status = 'offline' }) => {
const config = {
online: { color: '#22c55e', label: '在線' },
busy: { color: '#f59e0b', label: '忙碌' },
offline: { color: '#94a3b8', label: '離線' },
};
const { color, label } = config[status] || config.offline;
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '0.8rem', color }}>
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: color, display: 'inline-block' }} />
{label}
</span>
);
};
// ============================================================
// 元件:技能標籤列表
// ============================================================
const SkillTags = ({ skills = [], maxShow = 3 }) => {
const visible = skills.slice(0, maxShow);
const remaining = skills.length - maxShow;
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', marginTop: '8px' }}>
{visible.map((skill) => (
<span
key={skill}
style={{
padding: '2px 10px',
borderRadius: '12px',
backgroundColor: '#f1f5f9',
color: '#475569',
fontSize: '0.75rem',
}}
>
{skill}
</span>
))}
{remaining > 0 && (
<span style={{ padding: '2px 10px', fontSize: '0.75rem', color: '#94a3b8' }}>
+{remaining}
</span>
)}
</div>
);
};
// ============================================================
// 元件:成員卡片
// ============================================================
const MemberCard = ({
name,
role,
department,
email,
skills,
status,
avatar,
isSelected,
onSelect,
onContact,
}) => (
<Card highlighted={isSelected} onClick={onSelect}>
<div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start' }}>
{/* 大頭貼 */}
<div
style={{
width: '56px',
height: '56px',
borderRadius: '12px',
backgroundColor: '#f1f5f9',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '1.8rem',
flexShrink: 0,
}}
>
{avatar}
</div>
{/* 資訊 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ margin: 0, fontSize: '1rem', color: '#0f172a' }}>{name}</h3>
<StatusIndicator status={status} />
</div>
<p style={{ margin: '2px 0', color: '#64748b', fontSize: '0.85rem' }}>
{role} · {department}
</p>
<SkillTags skills={skills} />
</div>
</div>
{/* 操作按鈕 */}
<div style={{ display: 'flex', gap: '8px', marginTop: '12px', paddingTop: '12px', borderTop: '1px solid #f1f5f9' }}>
<button
onClick={(e) => {
e.stopPropagation();
onContact('email', email);
}}
style={{
flex: 1,
padding: '6px',
borderRadius: '6px',
border: '1px solid #e2e8f0',
backgroundColor: 'white',
color: '#475569',
cursor: 'pointer',
fontSize: '0.8rem',
}}
>
📧 Email
</button>
<button
onClick={(e) => {
e.stopPropagation();
onContact('chat', name);
}}
style={{
flex: 1,
padding: '6px',
borderRadius: '6px',
border: 'none',
backgroundColor: '#6366f1',
color: 'white',
cursor: 'pointer',
fontSize: '0.8rem',
}}
>
💬 聊天
</button>
</div>
</Card>
);
// ============================================================
// 元件:篩選按鈕列
// ============================================================
const FilterBar = ({ departments, selectedDept, onFilterChange }) => (
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '20px' }}>
{departments.map((dept) => (
<button
key={dept}
onClick={() => onFilterChange(dept)}
style={{
padding: '6px 16px',
borderRadius: '20px',
border: 'none',
backgroundColor: selectedDept === dept ? '#4f46e5' : '#f1f5f9',
color: selectedDept === dept ? 'white' : '#64748b',
cursor: 'pointer',
fontSize: '0.85rem',
fontWeight: selectedDept === dept ? '600' : '400',
}}
>
{dept === 'All' ? '全部' : dept}
</button>
))}
</div>
);
// ============================================================
// 元件:詳細資訊面板
// ============================================================
const DetailPanel = ({ member }) => {
if (!member) {
return (
<div style={{
padding: '40px',
textAlign: 'center',
color: '#94a3b8',
backgroundColor: '#f8fafc',
borderRadius: '12px',
border: '2px dashed #e2e8f0',
}}>
<p style={{ fontSize: '2rem', margin: '0 0 8px' }}>👈</p>
<p>點擊成員卡片查看詳細資訊</p>
</div>
);
}
return (
<div style={{
padding: '24px',
backgroundColor: '#f8fafc',
borderRadius: '12px',
border: '1px solid #e2e8f0',
}}>
<div style={{ textAlign: 'center', marginBottom: '20px' }}>
<div style={{ fontSize: '3rem', marginBottom: '8px' }}>{member.avatar}</div>
<h2 style={{ margin: '0 0 4px', color: '#0f172a' }}>{member.name}</h2>
<p style={{ margin: 0, color: '#64748b' }}>{member.role}</p>
<StatusIndicator status={member.status} />
</div>
<div style={{ borderTop: '1px solid #e2e8f0', paddingTop: '16px' }}>
<InfoRow label="部門" value={member.department} />
<InfoRow label="Email" value={member.email} />
<InfoRow label="電話" value={member.phone} />
<div style={{ marginTop: '12px' }}>
<span style={{ color: '#64748b', fontSize: '0.85rem' }}>技能:</span>
<SkillTags skills={member.skills} maxShow={10} />
</div>
</div>
</div>
);
};
const InfoRow = ({ label, value }) => (
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '6px 0', fontSize: '0.9rem' }}>
<span style={{ color: '#64748b' }}>{label}</span>
<span style={{ color: '#1e293b', fontWeight: '500' }}>{value}</span>
</div>
);
// ============================================================
// App:組合所有元件
// ============================================================
function App() {
const [selectedId, setSelectedId] = useState(null);
const [filterDept, setFilterDept] = useState('All');
const departments = ['All', ...new Set(teamMembers.map((m) => m.department))];
const filteredMembers =
filterDept === 'All'
? teamMembers
: teamMembers.filter((m) => m.department === filterDept);
const selectedMember = teamMembers.find((m) => m.id === selectedId) || null;
const handleContact = (method, target) => {
alert(`透過 ${method} 聯繫 ${target}`);
};
return (
<div
style={{
maxWidth: '900px',
margin: '0 auto',
padding: '32px 20px',
fontFamily: "'Segoe UI', 'Noto Sans TC', sans-serif",
}}
>
<h1 style={{ color: '#0f172a', marginBottom: '4px' }}>團隊通訊錄</h1>
<p style={{ color: '#94a3b8', marginTop: 0 }}>{teamMembers.length} 位成員</p>
<FilterBar
departments={departments}
selectedDept={filterDept}
onFilterChange={setFilterDept}
/>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap: '24px', alignItems: 'start' }}>
{/* 成員列表 */}
<div>
{filteredMembers.map((member) => (
<MemberCard
key={member.id}
{...member}
isSelected={selectedId === member.id}
onSelect={() => setSelectedId(member.id)}
onContact={handleContact}
/>
))}
{filteredMembers.length === 0 && (
<p style={{ textAlign: 'center', color: '#94a3b8', padding: '40px' }}>
沒有找到成員
</p>
)}
</div>
{/* 詳細面板 */}
<DetailPanel member={selectedMember} />
</div>
</div>
);
}
export default App;
Artigos relacionados
App.js
App.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.js).
Ler artigo →App.test.js
App.test.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.test.js).
Ler artigo →index.js
index.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/index.js).
Ler artigo →reportWebVitals.js
reportWebVitals.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/reportWebVitals.js).
Ler artigo →setupTests.js
setupTests.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/setupTests.js).
Ler artigo →Project_HelloReact.jsx
Project_HelloReact.jsx — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/projects/Project_HelloReact.jsx).
Ler artigo →