iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
Modern Web

Angular、React、Vue 三框架實戰養成:從零打造高品質前端履歷網站系列 第 6

Day 6 小專案:純原生 HTML/CSS/TS 的自我介紹頁(可交付 MVP) 今日目標

  • 分享至 

  • xImage
  •  

今日目標

把 Day1–Day4 的內容整合,完成一個 無框架的一頁式自我介紹網站,包含:

  • 語義化結構(header / nav / main / section / article / aside / footer)
  • RWD 版面與 Design Tokens(色彩、字體、間距)
  • 主題切換(亮/暗,含記憶偏好)
  • 導覽錨點與平滑捲動
  • 技能分類篩選
  • 照片切換(正式照/生活照)
  • 簡單表單驗證(姓名、Email、訊息)

完成後,你就擁有一個「無框架也能上線」的作品。明天起把同一份 IA 搬進 Angular。


專案結構與檔案

day5-portfolio/
├─ index.html
├─ styles/
│  └─ style.css       # 可先用 CSS;之後你要換 SCSS 也行
└─ scripts/
   └─ main.js         # 以 TS 思維撰寫(若要 TS 編譯可命名 main.ts)

提醒:如果你偏好 TypeScript,請以 main.ts 撰寫後用 tsc 編譯成 main.js,並在 HTML 載入 main.js 即可(寫法與 Day 3/Day 4 一致)。


完整實作

index.html

語義化骨架 + 錨點導覽 + About / Skills / Projects / Contact 區塊一次就位。

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Chiayu · 前端工程師 · 履歷網站</title>
  <meta name="description" content="Chiayu 的前端個人網站與作品集,含技能、專案與聯絡方式。" />
  <link rel="stylesheet" href="styles/style.css" />
</head>
<body>
  <header class="site-header">
    <div class="container">
      <a class="brand" href="#home" aria-label="回到頁面頂部">Chiayu</a>

      <nav class="site-nav" aria-label="主選單">
        <ul>
          <li><a href="#about">關於我</a></li>
          <li><a href="#skills">技能</a></li>
          <li><a href="#projects">作品</a></li>
          <li><a href="#contact">聯絡</a></li>
        </ul>
      </nav>

      <button id="theme-toggle" type="button" aria-pressed="false">切換主題</button>
    </div>
  </header>

  <main id="home">
    <!-- Hero -->
    <section class="hero container" aria-labelledby="hero-title">
      <div class="hero-text">
        <h1 id="hero-title">哈囉,我是 Chiayu</h1>
        <p>前端工程師|專長 Angular & TypeScript|喜歡把想法做成能上線的產品。</p>
        <div class="hero-cta">
          <a class="btn" href="#projects">看作品</a>
          <a class="btn btn-outline" href="#contact">聯絡我</a>
        </div>
      </div>
      <div class="hero-photo">
        <img id="avatar"
             src="assets/me-formal.jpg"
             data-alt-src="assets/me-casual.jpg"
             alt="Chiayu 的正式照片" width="240" height="240" />
        <button id="photo-toggle" class="btn small" type="button">切換照片</button>
      </div>
    </section>

    <!-- About -->
    <section id="about" class="container section" aria-labelledby="about-title">
      <h2 id="about-title">關於我</h2>
      <p>
        我是一名前端工程師,喜歡理解使用者需求並把它落地成產品。近期專注於
        Angular、TypeScript、前端架構與效能最佳化。
      </p>
      <blockquote class="quote">「持續學習,讓自己比昨天更強。」</blockquote>
      <p id="more-info" hidden>
        曾參與金融科技與電商專案,也投入設計系統與可存取性。閒暇時間喜歡健身、魔術與寫作分享。
      </p>
      <button id="toggle-more" class="btn small" type="button" aria-expanded="false" aria-controls="more-info">
        更多介紹
      </button>
    </section>

    <!-- Skills -->
    <section id="skills" class="container section" aria-labelledby="skills-title">
      <div class="section-header">
        <h2 id="skills-title">技能 Skillset</h2>
        <div id="skill-filters" role="tablist" aria-label="技能分類">
          <button role="tab" data-filter="all" aria-selected="true" class="chip">全部</button>
          <button role="tab" data-filter="frontend" class="chip">前端</button>
          <button role="tab" data-filter="backend" class="chip">後端</button>
          <button role="tab" data-filter="tools" class="chip">工具</button>
        </div>
      </div>

      <ul id="skill-list" class="skill-grid">
        <li data-category="frontend">HTML / CSS / SCSS</li>
        <li data-category="frontend">TypeScript</li>
        <li data-category="frontend">Angular / React / Vue</li>
        <li data-category="backend">Node.js / Express</li>
        <li data-category="tools">Git / GitHub / Docker</li>
        <li data-category="tools">Vite / Webpack</li>
      </ul>
    </section>

    <!-- Projects -->
    <section id="projects" class="container section" aria-labelledby="projects-title">
      <h2 id="projects-title">作品集 Projects</h2>
      <div class="project-grid">
        <article class="card">
          <h3>毛毛購物(寵物電商)</h3>
          <p class="muted">Angular + Node.js|購物車、結帳、RWD</p>
          <p>主導前端架構,完成商品列表、購物流程與訂單頁。</p>
          <a class="btn small" href="#" aria-label="查看毛毛購物專案">Live Demo</a>
        </article>

        <article class="card">
          <h3>LINE Bot 預約系統</h3>
          <p class="muted">Cloud Functions + LINE API|時段預約</p>
          <p>整合 LINE 聊天介面與雲端排程,完成會員預約流程。</p>
          <a class="btn small" href="#" aria-label="查看 LINE Bot 預約系統">Live Demo</a>
        </article>
      </div>
    </section>

    <!-- Contact -->
    <section id="contact" class="container section" aria-labelledby="contact-title">
      <h2 id="contact-title">聯絡我</h2>
      <form id="contact-form" novalidate>
        <div class="field">
          <label for="name">姓名</label>
          <input id="name" name="name" type="text" placeholder="王小明" required />
          <small class="error" data-for="name" hidden>請輸入 2–20 個字的姓名</small>
        </div>

        <div class="field">
          <label for="email">Email</label>
          <input id="email" name="email" type="email" placeholder="name@example.com" required />
          <small class="error" data-for="email" hidden>請輸入正確的 Email</small>
        </div>

        <div class="field">
          <label for="message">訊息</label>
          <textarea id="message" name="message" rows="4" placeholder="想合作的內容…" required></textarea>
          <small class="error" data-for="message" hidden>訊息至少 10 個字</small>
        </div>

        <div class="actions">
          <button class="btn" type="submit">送出</button>
          <button class="btn btn-outline" type="reset">清除</button>
        </div>
      </form>

      <aside class="contact-aside">
        <h3>其他聯絡方式</h3>
        <address>
          Email:<a href="mailto:hotdanton08@hotmail.com">hotdanton08@hotmail.com</a><br />
          GitHub:<a href="https://github.com/你的帳號">github.com/你的帳號</a><br />
          LinkedIn:<a href="https://linkedin.com/in/你的帳號">linkedin.com/in/你的帳號</a>
        </address>
      </aside>
    </section>
  </main>

  <footer class="site-footer">
    <div class="container">
      <p>&copy; 2025 Chiayu Lee · All rights reserved.</p>
    </div>
  </footer>

  <script src="scripts/main.js" defer></script>
</body>
</html>


styles/style.css

第一版以 CSS 即可(你也能把它翻成 SCSS,抽成變數與 mixin)。包含 Design Tokens、排版、RWD、主題切換掛鉤。

/* Design Tokens */
:root {
  --bg: #ffffff;
  --fg: #1f2937;
  --muted: #6b7280;
  --primary: #2563eb;
  --border: #e5e7eb;
  --container: 1100px;
  --radius: 12px;
  --space: 16px;
}

html[data-theme="dark"] {
  --bg: #0f172a;
  --fg: #e5e7eb;
  --muted: #9ca3af;
  --primary: #60a5fa;
  --border: #1f2937;
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: system-ui, -apple-system, "Noto Sans TC", Arial, sans-serif;
  line-height: 1.7;
}

/* Helpers */
.container { max-width: var(--container); margin: 0 auto; padding: 0 20px; }
.section { padding: 64px 0 48px; }
.muted { color: var(--muted); }
.btn {
  display: inline-block; border: 1px solid var(--primary); color: #fff;
  background: var(--primary); padding: 10px 16px; border-radius: 8px; text-decoration: none;
}
.btn:hover { opacity: .9; }
.btn-outline { background: transparent; color: var(--primary); }
.btn.small { padding: 6px 10px; font-size: 14px; }

/* Header */
.site-header { position: sticky; top: 0; background: var(--bg); border-bottom: 1px solid var(--border); z-index: 10; }
.site-header .container { display: flex; align-items: center; gap: 16px; padding: 12px 20px; }
.brand { font-weight: 700; color: var(--fg); text-decoration: none; }
.site-nav ul { list-style: none; margin: 0; padding: 0; display: flex; gap: 16px; }
.site-nav a { color: var(--fg); text-decoration: none; }
.site-nav a:hover { color: var(--primary); }
#theme-toggle { margin-left: auto; }

/* Hero */
.hero { display: grid; grid-template-columns: 1.2fr .8fr; align-items: center; gap: 24px; padding: 48px 0; }
.hero-cta { display: flex; gap: 12px; margin-top: 12px; }
.hero-photo { text-align: center; }
.hero-photo img { border-radius: 50%; border: 4px solid var(--border); }

/* Sections */
.section-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.quote { margin: 8px 0 0; padding-left: 12px; border-left: 4px solid var(--primary); color: var(--muted); }

/* Chips / Filters */
#skill-filters { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
  border: 1px solid var(--border); background: transparent; color: var(--fg);
  padding: 6px 10px; border-radius: 999px; cursor: pointer;
}
.chip[aria-selected="true"] { border-color: var(--primary); color: var(--primary); }

/* Grids */
.skill-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; padding: 0; list-style: none; }
.skill-grid li {
  border: 1px solid var(--border); padding: 10px 12px; border-radius: var(--radius);
}
.project-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.card { border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; background: rgba(255,255,255,0.02); }

/* Contact */
.field { margin-bottom: 12px; }
label { display: block; margin-bottom: 6px; }
input, textarea {
  width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px; background: transparent; color: var(--fg);
}
.error { color: #ef4444; display: block; margin-top: 6px; }

/* Aside */
.contact-aside { margin-top: 24px; border-top: 1px solid var(--border); padding-top: 16px; }

/* Footer */
.site-footer { margin-top: 48px; border-top: 1px solid var(--border); }
.site-footer .container { padding: 16px 20px; text-align: center; color: var(--muted); }

/* RWD */
@media (max-width: 900px) {
  .hero { grid-template-columns: 1fr; text-align: center; }
  .skill-grid { grid-template-columns: repeat(2, 1fr); }
  .project-grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
  .skill-grid { grid-template-columns: 1fr; }
}


scripts/main.js

以 Day 3、Day 4 的作法組合:主題切換、錨點平滑捲動、照片切換、更多介紹、技能篩選、表單驗證。

// 主題切換
const htmlEl = document.documentElement;
const themeBtn = document.querySelector('#theme-toggle');

function applyTheme(theme) {
  htmlEl.setAttribute('data-theme', theme);
  themeBtn?.setAttribute('aria-pressed', String(theme === 'dark'));
  if (themeBtn) themeBtn.textContent = theme === 'dark' ? '切換為亮色' : '切換為暗色';
  localStorage.setItem('theme', theme);
}
applyTheme(localStorage.getItem('theme') || 'light');
themeBtn?.addEventListener('click', () => {
  const next = htmlEl.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
  applyTheme(next);
});

// 導覽錨點:平滑捲動
document.querySelectorAll('a[href^="#"]').forEach(a => {
  a.addEventListener('click', (e) => {
    const id = a.getAttribute('href');
    if (!id || id === '#') return;
    const target = document.querySelector(id);
    if (target) {
      e.preventDefault();
      target.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }
  });
});

// 照片切換(正式/生活)
const avatar = document.querySelector('#avatar');
document.querySelector('#photo-toggle')?.addEventListener('click', () => {
  if (!avatar) return;
  const current = avatar.getAttribute('src') || '';
  const altSrc = avatar.dataset.altSrc || '';
  if (!altSrc) return;
  avatar.setAttribute('src', altSrc);
  avatar.dataset.altSrc = current;
  const isFormal = /formal/.test(altSrc);
  avatar.alt = isFormal ? 'Chiayu 的正式照片' : 'Chiayu 的生活照片';
});

// 更多介紹展開/收起
const more = document.querySelector('#more-info');
const moreBtn = document.querySelector('#toggle-more');
moreBtn?.addEventListener('click', () => {
  if (!more) return;
  const isHidden = more.hasAttribute('hidden');
  if (isHidden) {
    more.removeAttribute('hidden');
    moreBtn.setAttribute('aria-expanded', 'true');
    moreBtn.textContent = '收起介紹';
  } else {
    more.setAttribute('hidden', '');
    moreBtn.setAttribute('aria-expanded', 'false');
    moreBtn.textContent = '更多介紹';
  }
});

// 技能分類篩選(事件委派)
const filters = document.querySelector('#skill-filters');
const skillList = document.querySelector('#skill-list');
function applySkillFilter(cat) {
  if (!skillList) return;
  skillList.querySelectorAll('li').forEach(li => {
    const c = li.dataset.category || 'frontend';
    li.style.display = (cat === 'all' || c === cat) ? '' : 'none';
  });
}
filters?.addEventListener('click', (e) => {
  const target = e.target;
  if (target.matches('button[data-filter]')) {
    filters.querySelectorAll('[aria-selected="true"]').forEach(el => el.setAttribute('aria-selected', 'false'));
    target.setAttribute('aria-selected', 'true');
    applySkillFilter(target.getAttribute('data-filter'));
  }
});
applySkillFilter('all');

// 表單驗證(姓名 2–20、email 格式、訊息 ≥ 10)
const form = document.querySelector('#contact-form');
function setError(name, show) {
  const err = form?.querySelector(`.error[data-for="${name}"]`);
  if (!err) return;
  err.hidden = !show;
}
form?.addEventListener('submit', (e) => {
  e.preventDefault();
  const name = form.querySelector('#name');
  const email = form.querySelector('#email');
  const message = form.querySelector('#message');

  let ok = true;
  if (!name.value || name.value.trim().length < 2 || name.value.trim().length > 20) {
    setError('name', true); ok = false;
  } else setError('name', false);

  const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRe.test(email.value)) { setError('email', true); ok = false; } else setError('email', false);

  if (!message.value || message.value.trim().length < 10) { setError('message', true); ok = false; } else setError('message', false);

  if (ok) {
    alert('已送出!感謝你的來信。');
    form.reset();
  }
});


成果檢查清單(你可在文末附圖)

  • 桌機、手機版截圖各一:Hero、Skills、Projects、Contact 區塊
  • 主題切換前後的對比圖
  • 技能分類篩選示意(全部/前端/後端/工具)
  • 照片切換示意
  • 表單錯誤訊息提示示意

小心踩雷(常見誤用 → 正確作法)

  1. <br>&nbsp; 撐版面

    錯誤: 多放 <br> 讓元素彼此拉開。

    正確: 用 CSS 的 margin/padding 控距離,或用網格/彈性排版。

  2. <a> 當按鈕(沒有 href

    錯誤: <a>送出</a>

    正確: 導頁用 <a href="...">;觸發動作用 <button type="button">

  3. 直接寫 innerHTML = userInput

    錯誤: 可能造成 XSS。

    正確: 使用 textContent ;確實需要插入 HTML 再做消毒/模板化。

  4. 忘記處理 null、濫用非空斷言 !

    錯誤:

document.querySelector('#x').addEventListener('click', fn);

正確: 先判斷元素存在再綁事件;或在載入尾端 defer

  1. 把樣式全部用行內 style

    錯誤: JS 裡大量 el.style.xxx = ...

    正確:classList 切換狀態,把視覺交給 CSS。


進一步練習(可選)

  • 在技能區加「關鍵字即時搜尋」輸入框
  • 導覽列在捲動時自動高亮當前區段(IntersectionObserver)
  • 表單送出以 fetch 打到 mock API(或用 Formspree 類服務)

下一步(Day 6 預告)

明天開始把這份 同版型的網站 搬進 Angular

  • 用 Angular CLI 建專案、建立路由與頁面骨架
  • 把今天的內容「資料化」成 JSON 或 TS 型別,透過元件綁定顯示
  • 逐步把「主題切換、技能篩選、表單驗證」用 Angular 的方式重做一次

上一篇
Day 5 RWD 響應式網頁設計 – 讓自我介紹頁在各裝置都好看
下一篇
Day 7 Angular 起手式 – 用 Angular CLI 建立專案骨架
系列文
Angular、React、Vue 三框架實戰養成:從零打造高品質前端履歷網站26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言