iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
Modern Web

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

Day 21 Vue 起手式 – 用 Vite + TypeScript 初始化專案,搬入網站骨架

  • 分享至 

  • xImage
  •  

今日目標

  • Vite + Vue + TypeScript 建立新專案
  • 認識 Vue 專案的基本結構與單檔元件 SFC (.vue)
  • 把 Day 6 的 HTML/CSS 先搬進 Vue(切成基本元件)
  • 跑起本地開發伺服器,看到第一版畫面

基礎概念(和 Angular 對照,幫你快速轉換思維)

  • Angular 有 CLI 幫你生成結構;Vue 使用 Vite(或 Vue CLI)快速開專案。
  • Angular 元件三件套(ts/html/scss),Vue 單檔元件 SFCtemplate / script / style 放同一檔。
  • Angular 以裝飾器管理元件;Vue 用 <script setup> 搭配 Composition API,語法更直覺。
  • 路由、表單、狀態管理在 Vue 是分模組挑選(vue-router、@vueuse、pinia…),按需裝

建立專案(Vite + Vue + TS)

# 1) 建立專案
npm create vite@latest resume-site-vue -- --template vue-ts

# 2) 進入資料夾並安裝依賴
cd resume-site-vue
npm install

# 3) 啟動開發伺服器
npm run dev

打開瀏覽器看到 Vite + Vue 的起始頁就成功了。


認識專案結構(最重要幾個檔)

resume-site-vue/
├─ index.html              # 整站入口(只掛 app 根節點)
├─ src/
│  ├─ main.ts              # Vue 進入點(建立 App、掛載)
│  ├─ App.vue              # 根元件(之後放 Header/Footer + <router-view>)
│  └─ assets/              # 靜態資源
└─ vite.config.ts

Vue 的畫面都從 App.vue 長出來。我們會把 Day 6 的頁面骨架切成多個 SFC,再由 App.vue 組裝。


先準備通用樣式(把 Day 6 的簡版 CSS 貼進來)

建檔 src/styles/base.css

把你在 Day 6 的「超簡版 CSS」貼進來(我稍微整理成 Vue/通用可用版):

/* src/styles/base.css */
/* Reset & base */
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font-family: system-ui, -apple-system, "Noto Sans TC", Arial, sans-serif;
  line-height: 1.7;
  color: #1f2937;
  background: #ffffff;
}
img { max-width: 100%; height: auto; display: block; }
a  { color: inherit; text-decoration: none; }

/* Layout helpers */
.container { max-width: 960px; margin: 0 auto; padding: 0 16px; }
.section { padding: 56px 0 40px; }
.muted { color: #6b7280; }

/* Buttons */
.btn { display:inline-block; padding:10px 16px; background:#2563eb; color:#fff; border:1px solid #2563eb; border-radius:8px; }
.btn:hover { opacity:.92; }
.btn-outline { background:transparent; color:#2563eb; border-color:#2563eb; }
.btn.small { padding:6px 10px; font-size:14px; }

/* Header */
.site-header { position:sticky; top:0; background:#fff; border-bottom:1px solid #e5e7eb; z-index:10; }
.site-header .container { display:flex; align-items:center; gap:16px; padding:12px 16px; }
.brand { font-weight:700; }
.site-nav { margin-left:auto; }
.site-nav ul { list-style:none; margin:0; padding:0; display:flex; gap:12px; }
.site-nav a:hover { color:#2563eb; }

/* Hero */
.hero { display:grid; gap:20px; padding:40px 0; }
.hero-text h1 { margin:8px 0 6px; font-size: clamp(24px, 5vw, 34px); }
.hero-cta { display:flex; gap:12px; margin-top:8px; }
.hero-photo { max-width:240px; }
.hero-photo img { border-radius:50%; border:4px solid #e5e7eb; }

/* Chips & grids */
.chip { padding:6px 10px; border-radius:999px; border:1px solid #e5e7eb; background:transparent; color:#1f2937; cursor:pointer; }
.chip[aria-selected="true"] { border-color:#2563eb; color:#2563eb; }

.skill-grid, .project-grid { display:grid; gap:12px; }
.skill-grid { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); padding:0; list-style:none; }
.skill-grid li { border:1px solid #e5e7eb; padding:10px 12px; border-radius:10px; }
.project-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.card { border:1px solid #e5e7eb; border-radius:12px; padding:16px; }

/* Forms */
.field { margin-bottom:12px; }
label { display:block; margin-bottom:6px; }
input, textarea { width:100%; padding:10px 12px; border:1px solid #e5e7eb; border-radius:8px; background:#fff; color:#1f2937; }
.error { color:#ef4444; display:block; margin-top:6px; }

/* Footer */
.site-footer { margin-top:48px; border-top:1px solid #e5e7eb; }
.site-footer .container { padding:16px; text-align:center; color:#6b7280; }

/* Single breakpoint */
@media (min-width: 900px) {
  .hero { grid-template-columns: 1.2fr .8fr; align-items: center; }
}

src/main.ts 引入

import { createApp } from 'vue'
import App from './App.vue'
import './styles/base.css'

createApp(App).mount('#app')


切出第一批 Vue 元件(SFC)

建立資料夾 src/components/,把頁面切成 7 個元件(和 Angular 對齊):

  • SiteHeader.vue
  • Hero.vue
  • About.vue
  • Skills.vue
  • Projects.vue
  • Contact.vue
  • SiteFooter.vue

src/App.vue

<template>
  <SiteHeader />

  <main id="home">
    <Hero />
    <About />
    <Skills />
    <Projects />
    <Contact />
  </main>

  <SiteFooter />
</template>

<script setup lang="ts">
import SiteHeader from './components/SiteHeader.vue'
import Hero from './components/Hero.vue'
import About from './components/About.vue'
import Skills from './components/Skills.vue'
import Projects from './components/Projects.vue'
import Contact from './components/Contact.vue'
import SiteFooter from './components/SiteFooter.vue'
</script>

src/components/SiteHeader.vue

<template>
  <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>
    </div>
  </header>
</template>

src/components/Hero.vue

<template>
  <section class="hero container" aria-labelledby="hero-title">
    <div class="hero-text">
      <h1 id="hero-title">哈囉,我是 Chiayu</h1>
      <p>前端工程師|專長 Angular / Vue / 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 :src="photo" :alt="alt" width="240" height="240" />
      <button class="btn small" type="button" @click="toggle">切換照片</button>
    </div>
  </section>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const formal = 'assets/me-formal.jpg'
const casual = 'assets/me-casual.jpg'
const photo = ref(formal)
const alt = ref('Chiayu 的正式照片')

function toggle() {
  if (photo.value === formal) {
    photo.value = casual
    alt.value = 'Chiayu 的生活照片'
  } else {
    photo.value = formal
    alt.value = 'Chiayu 的正式照片'
  }
}
</script>

src/components/About.vue

<template>
  <section id="about" class="container section" aria-labelledby="about-title">
    <h2 id="about-title">關於我</h2>
    <p>
      我是一名前端工程師,喜歡理解使用者需求並把它落地成產品。近期專注於
      Vue、Angular、TypeScript、前端架構與效能最佳化。
    </p>
    <blockquote class="quote">「持續學習,讓自己比昨天更強。」</blockquote>

    <p v-if="isOpen" id="more-info">
      曾參與金融科技與電商專案,也投入設計系統與可存取性。閒暇時間喜歡健身、魔術與寫作分享。
    </p>
    <button class="btn small" type="button"
            :aria-expanded="isOpen.toString()"
            aria-controls="more-info"
            @click="isOpen = !isOpen">
      {{ isOpen ? '收起介紹' : '更多介紹' }}
    </button>
  </section>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const isOpen = ref(false)
</script>

src/components/Skills.vue

先放靜態清單(明天開始做資料化、篩選、v-model 互動):

<template>
  <section id="skills" class="container section" aria-labelledby="skills-title">
    <div class="section-header">
      <h2 id="skills-title">技能 Skillset</h2>
      <div role="tablist" aria-label="技能分類" class="filters">
        <button class="chip" aria-selected="true">全部</button>
        <button class="chip">前端</button>
        <button class="chip">後端</button>
        <button class="chip">工具</button>
      </div>
    </div>

    <ul class="skill-grid">
      <li>HTML / CSS / SCSS</li>
      <li>TypeScript</li>
      <li>Vue / Angular / React</li>
      <li>Node.js / Express</li>
      <li>Git / GitHub / Docker</li>
      <li>Vite / Webpack</li>
    </ul>
  </section>
</template>

src/components/Projects.vue

<template>
  <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">Vue + Node.js|購物車、結帳、RWD</p>
        <p>主導前端架構,完成商品列表、購物流程與訂單頁。</p>
        <a class="btn small" href="#" target="_blank">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="#" target="_blank">Live Demo</a>
      </article>
    </div>
  </section>
</template>

src/components/Contact.vue

<template>
  <section id="contact" class="container section" aria-labelledby="contact-title">
    <h2 id="contact-title">聯絡我</h2>
    <form @submit.prevent="submit" novalidate>
      <div class="field">
        <label for="name">姓名</label>
        <input id="name" v-model="name" type="text" placeholder="王小明" required />
        <small class="error" v-if="errors.name">請輸入 2–20 個字的姓名</small>
      </div>
      <div class="field">
        <label for="email">Email</label>
        <input id="email" v-model="email" type="email" placeholder="name@example.com" required />
        <small class="error" v-if="errors.email">請輸入正確的 Email</small>
      </div>
      <div class="field">
        <label for="message">訊息</label>
        <textarea id="message" v-model="message" rows="4" placeholder="想合作的內容…" required></textarea>
        <small class="error" v-if="errors.message">訊息至少 10 個字</small>
      </div>
      <div class="actions">
        <button class="btn" type="submit">送出</button>
        <button class="btn btn-outline" type="button" @click="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/你的帳號" target="_blank">github.com/你的帳號</a><br />
        LinkedIn:<a href="https://linkedin.com/in/你的帳號" target="_blank">linkedin.com/in/你的帳號</a>
      </address>
    </aside>
  </section>
</template>

<script setup lang="ts">
import { reactive, ref } from 'vue'

const name = ref('')
const email = ref('')
const message = ref('')
const errors = reactive({ name:false, email:false, message:false })

function submit() {
  errors.name = name.value.trim().length < 2 || name.value.trim().length > 20
  errors.email = !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)
  errors.message = message.value.trim().length < 10

  if (errors.name || errors.email || errors.message) return
  alert('已送出!感謝你的來信。')
  reset()
}
function reset() {
  name.value = ''; email.value = ''; message.value = ''
  errors.name = errors.email = errors.message = false
}
</script>

src/components/SiteFooter.vue

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


成果檢查清單

  • npm run dev 後能看到與 Day 6 類似的畫面(Vue 版本)。
  • Header/Hero/About/Skills/Projects/Contact/Footer 都是 SFC,從 App.vue 組裝。
  • Hero 的「切換照片」、About 的「更多介紹」、Contact 的表單驗證已能基本互動。

小心踩雷(新手常見)

  1. 把內容寫到 index.html
    • Vue 的畫面應該寫在 App.vue/元件裡,index.html 只負責 <div id="app"> 掛載點。
  2. 樣式沒套到
    • 忘了在 main.ts 引入 ./styles/base.css
  3. 圖片路徑錯誤
    • assets 放在 public/src/assets/ 都可;若 404,先用開發工具看最終路徑。
  4. TypeScript 提示一堆
    • 建議先照範例型別走,遇到 any 再局部放寬,不要一開始就關掉 TS。

下一步(Day 22 預告)

明天我們要把資料「資料化 + 綁定」:

  • ref/reactive 管理 skills、projects 陣列,改用 v-for 渲染清單
  • 實作分類按鈕 + 關鍵字搜尋(computed + v-model
  • 開始規劃 vue-router 的路由表,為 Day 23 的詳情頁做準備

上一篇
Day 20 Angular 版履歷網站完成與部署
下一篇
Day 22 Vue 資料綁定 – 用 v-for 渲染清單 + 分類與搜尋
系列文
Angular、React、Vue 三框架實戰養成:從零打造高品質前端履歷網站22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言