<script setup>
搭配 Composition API,語法更直覺。# 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 組裝。
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')
建立資料夾 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>© 2025 Chiayu Lee · All rights reserved.</p>
</div>
</footer>
</template>
npm run dev
後能看到與 Day 6 類似的畫面(Vue 版本)。App.vue
組裝。index.html
App.vue
/元件裡,index.html
只負責 <div id="app">
掛載點。main.ts
引入 ./styles/base.css
。assets
放在 public/
或 src/assets/
都可;若 404,先用開發工具看最終路徑。明天我們要把資料「資料化 + 綁定」:
ref
/reactive
管理 skills、projects 陣列,改用 v-for
渲染清單computed
+ v-model
)vue-router
的路由表,為 Day 23 的詳情頁做準備