在前一篇我們完成 Nuxt 專案初始化,以及介紹到目錄結構,這篇將依照設計稿,一步步完成整體畫面製作。本篇會專注在網站註冊頁面 UI 元件的開發,例如 條款提示視窗、插圖視覺區塊、LINE 按鈕設計等,建立好畫面基礎,為後續加上 LIFF 互動功能做好準備💪!
pages
的放置位置與檔案命名會直接對應形成網址路徑(例如 /user/register
),因此SFC檔名建議以小寫+中橫線(kebab-case) 來命名,當在 Linux 系統訪問網站時,就不會有大小寫混淆,訪問不到位址的問題。pages
資料夾,以及在建立好的資料夾新增 signup.vue
檔案:pages/
├── signup.vue
這邊可能有人會很好奇,既然官方已經有定義好專案目錄結構了,為什麼 Nuxt 在一開始,不幫我們自動建立好所有目錄?
Nuxt 採取「約定大於配置」的設計架構,設計目標是盡可能保持輕量,它不強迫開發者在一開始就建立所有目錄,而是讓開發者自行去選擇,建立「實際需要使用」的部分,而不是一開始就在專案初始化時就自動產出一堆空資料夾。例如 假設你只想建立一個沒有多個頁面的單頁應用程式(SPA),就不需要建立
pages/
資料夾,而只需使用app.vue
。
npm install @nuxt/ui
安裝完套件後,記得在 nuxt.config.ts
裡設定好 模組與 CSS 引入。
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css']
})
接下來,新增 assets/css
資料夾與 main.css
檔案,並在 main.css
中引入 Nuxt UI 的樣式:
......
@import "@nuxt/ui";
完成後再重新執行 npm run dev
http://localhost:3000
,頁面會呈現404 - Page not found: / 🤔,這是因為目前根路徑 / 還沒有對應的頁面檔案。前面有提到 Nuxt 採用 基於檔案的路由(file-based routing),會根據 pages/
目錄下的檔案,自動生成對應的路由。我們目前只有新增 pages/signup.vue
,所以訪問要輸入:http://localhost:3000/signup
(回到開始畫面)
app.vue
,註解或移除掉預設內容的兩個元件,加上 <NuxtPage />
。<NuxtPage />
是 Nuxt 提供的自動路由頁面插槽元件,會根據 pages/
目錄自動載入對應頁面。<!-- App.vue -->
<template>
<NuxtPage />
</template>
額外補充:如果有要套用 layout
,請在外層加上 NuxtLayout
:
<!-- App.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
assets
內新增 images
資料夾,並放入 gradient-bg.png
、line-logo.svg
圖檔。assets/
├── images/
│ └── gradient-bg.png
│ └── line-logo.svg
接著打開 signup.vue
,我們來製作一個「雙欄式的登入/註冊畫面」:
外層 <main>
是整體容器,並透過 md:flex-row
實作出響應式的雙欄排版,當畫面寬度到達中型裝置(md)以上時,會由直式排版轉為左右雙欄。
根據設計稿,左側是插圖區塊,背景設計採用 bg-gradient-to-r from-[#6b4d6b] to-[#2a3e68]
建立線性漸層背景,從紫色漸變到藍色。使用 flex items-center justify-center
將圖片置中。圖片使用 import
將素材載入,交由 Nuxt 編譯處理,讓變數 gradientBg
綁定圖片來源。
右側為註冊功能區塊,設定內距 p-8
,同樣以 md:w-1/2
響應式設計呈現。製作綠色背景的 LINE 註冊按鈕,與品牌視覺風格一致,LINE 圖示透過 import
導入 lineLogo
,放在按鈕內。點擊後會觸發 onLineSignupClick
方法(該方法在 <script setup>
裡定義)。
<template>
<main class="min-h-screen flex flex-col md:flex-row">
<div class="w-full md:w-1/2 bg-gradient-to-r from-[#6b4d6b] to-[#2a3e68] p-8 flex justify-center">
<div class="max-w-2xl mt-8">
<div class="rounded-md overflow-hidden">
<img
:src="gradientBg"
alt="充滿童趣風格的海底插圖,描繪章魚、鯊魚、熱帶魚與海星在深海中悠游。"
class="w-lg h-auto"
/>
</div>
</div>
</div>
<div class="w-full md:w-1/2 bg-white p-8 flex justify-center font-normal">
<div class="max-w-md w-full mt-30">
<h3 class="text-3xl font-extrabold text-[#525252] text-center mb-3">立即註冊</h3>
<p class="text-center mb-4 text-[#525252] font-medium">歡迎!透過 <span class=" text-[#00c300]">LINE</span> 註冊,開始使用服務吧。</p>
<button @click="onLineSignupClick" class="lineLogo h-12 w-full bg-[#00c300] cursor-pointer font-bold text-white py-3 px-4 rounded-md flex items-center justify-center mb-4">
<img
alt="LINE註冊登入圖示"
:src="lineLogo"
width=24
height=24
class="mr-2"
/>
LINE 註冊
</button>
<div class="bg-[#e7eaef] h-12 p-4 rounded-md text-center mb-2 flex items-center justify-center text-center">
<span class="text-[#525252] font-medium">已有帳號了?</span>
<a href="#" class="text-[#2a68c8] ml-2 hover:underline font-bold">
在此登入
</a>
</div>
<div class="text-start text-[#aeada9] font-medium">
<a href="#" class="hover:underline">
隱私權政策
</a>
<span class="mx-1">|</span>
<a href="#" class="hover:underline">
服務條款聲明
</a>
</div>
</div>
</div>
</main>
</template>
<script setup>
import gradientBg from '@/assets/images/gradient-bg.png'
import lineLogo from '@/assets/images/line-logo.svg'
let onLineSignupClick = async()=> {
// 等等下方會分享到內容
}
</script>
Header.vue
導覽列元件,放入 components
資料夾中。components
、public
資料夾,再新增Header.vue
,Nuxt 會自動註冊 components/
目錄中的元件。建議使用大駝峰命名(PascalCase),以避免與原生 HTML 標籤名稱衝突。components/
├── public/
│ └── Header.vue
打開 Header.vue
撰寫導覽列內容:
<template>
<header class="w-full bg-white py-3 px-6 flex justify-between items-center">
<div class="flex items-center justify-center space-x-6 font-medium">
<a href="/" class="flex items-center">
<img
src="/public/logo.png"
alt="Sea Museum"
width=176
height=65
class="mr-2"
/>
</a>
<nav class="hidden md:flex items-center space-x-8 font-medium">
<a href="#" class="text-[#525252] font-normal tracking-normal font-sans text-center">
展區介紹
</a>
<a href="#" class="text-[#525252] font-normal tracking-normal font-sans text-center">
海洋保育專區
</a>
<a href="#" class="text-[#525252] font-normal tracking-normal font-sans text-center">
參觀資訊
</a>
<a href="#" class="text-[#525252] font-normal tracking-normal font-sans text-center">
最新消息
</a>
</nav>
</div>
<div class="flex items-center">
<svg class="text-[#525252]" xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5em" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path fill="currentColor" d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4" /></svg>
</div>
</header>
</template>
(上方程式中 Icon來源)
signup.vue
,在頁面中加入我們剛剛建立好的 <PublicHeader />
元件。 <template>
<PublicHeader />
<main class="min-h-screen flex flex-col md:flex-row">
......
目前為止,註冊 UI 成果如下:
npm install sweetalert2
新增 plugins/
資料夾,並建立一個 plugin 檔案 sweetalert.js
,這樣 SweetAlert2
就可以在任何 Vue 元件中使用。
import Swal from 'sweetalert2'
export default defineNuxtPlugin(() => {
return {
provide: {
swal: Swal
}
}
})
provide
將外部功能注入到 NuxtApp
。因此在元件內,我們需要使用 useNuxtApp()
來取得 $swal
,並在註冊頁的 onLineSignupClick
方法中加入 SweetAlert
的彈窗內容:title
:彈窗標題icon
:標題上方的圖示,還有 info
, success
, warning
, error
其他值可以使用。width
:可調整彈窗寬度html
:可自訂 HTML 製作彈窗內容<script setup>
....
const { $swal } = useNuxtApp()
const onLineSignupClick = async()=> {
await $swal.fire({
title: "隱私權政策與服務條款聲明",
icon: "info",
width: "90%",
html: `<div style="text-align: left; max-height: 60vh; overflow-y: auto; padding-right: 8px;">
<h3 class="text-3xl font-bold text-[#3FC3EE] mb-3">隱私權政策</h3>
<h4 class="text-xl font-bold">一、適用範圍</h4>
<p>本政策適用於您在使用本網站所提供的各項服務時,所涉及的個人資料收集、處理與利用。</p>
<br/>
<h4 class="text-xl font-bold">二、個人資料蒐集目的與類型</h4>
<p>本網站將蒐集下列資訊以提供會員服務、訂單處理、客服聯繫與行銷通知:</p>
<ul>
<li>1. 姓名、聯絡電話、電子郵件等聯絡資料</li>
<li>2. 登入資訊、IP 位址、瀏覽紀錄、裝置資訊等使用行為紀錄</li>
</ul>
<br/>
<h4 class="text-xl font-bold">三、資料使用方式</h4>
<p>蒐集之個人資料僅用於提供服務與行銷推播,並依法律規定處理與保護。</p>
<br/>
<h4 class="text-xl font-bold">四、資料保存與安全</h4>
<ul>
<li>1. 您的個人資料將依據業務需求與法令規定保存,期間屆滿後將刪除或匿名化處理。</li>
<li>2. 本網站採取合適的資安措施,以防止個資被未授權存取、洩漏或篡改。</li>
</ul>
<br/>
<h4 class="text-xl font-bold">五、個人資料權利</h4>
<p>依據《個人資料保護法》,您對所提供的個人資料擁有以下權利:</p>
<ul>
<li>1. 查詢或請求閱覽。</li>
<li>2. 請求製給複製本。</li>
<li>3. 請求補充或更正。</li>
<li>4. 請求停止蒐集、處理或利用。</li>
<li>5. 請求刪除。</li>
</ul>
<br/>
<h4 class="text-xl font-bold">六、Cookie 技術使用</h4>
<p>為提升使用體驗,網站可能使用 Cookie 技術,可透過瀏覽器設定限制。</p>
<br/>
<h4 class="text-xl font-bold">七、政策修訂</h4>
<p>本政策得隨時修改,變更內容將公告於本網站,不另行個別通知,請定期查閱。</p>
<hr class="text-gray-300 border-2 my-4">
<h3 class="text-3xl font-bold text-[#3FC3EE] mb-3">服務條款</h3>
<h4 class="text-xl font-bold">一、接受條款</h4>
<p>歡迎您使用本網站(以下簡稱「本網站」)所提供的服務。當您註冊為會員、或使用本網站任何服務,即表示您已閱讀、了解並同意遵守本服務條款。若您不同意,請勿使用本網站服務。</p>
<br/>
<h4 class="text-xl font-bold">二、會員帳號與密碼</h4>
<p>使用本網站前,您須提供正確個人資訊以完成註冊,若因帳號使用不當導致損害,概由會員自行負責。</p>
<br/>
<h4 class="text-xl font-bold">三、使用規範</h4>
<p>1. 您不得利用本網站從事任何非法、侵權或違反公共秩序善良風俗之行為。</p>
<p>2. 本網站有權對違反條款之帳號採取限制、終止服務等處置,恕不另行通知。</p>
<br/>
<h4 class="text-xl font-bold">四、服務內容變更</h4>
<p>本網站保留隨時修改、暫停或終止全部或部分服務內容之權利,並不負事先通知之義務。</p>
<br/>
<h4 class="text-xl font-bold">五、免責聲明</h4>
<ul>
<li>1. 對於因系統維護、天災、駭客攻擊等不可抗力因素所致的服務中斷或資料遺失,本網站不負任何賠償責任。</li>
<li>2. 本網站對於使用者上傳或張貼的內容,無須負事先審查義務,若發現違法情事將依法處理。</li>
</ul>
<br/>
<h4 class="text-xl font-bold">六、智慧財產權</h4>
<p>本網站所有內容(含文字、圖像、設計、程式碼等)均為本網站或其合法權利人所有,未經授權不得任意使用、重製或散布。</p>
<br/>
<h4 class="text-xl font-bold">七、準據法與管轄權</h4>
<p>本服務條款之解釋與適用,以中華民國法律為準據法,並以本網站營運所在地之法院為第一審管轄法院。</p>
</div>`,
showCloseButton: true,
showCancelButton: true,
focusConfirm: false,
confirmButtonText: `同意`,
cancelButtonText: `不同意`,
})
}
</script>
pages/
資料夾下,新增 member/
子目錄,並建立 index.vue
作為會員首頁。pages/
├── member/
│ └── index.vue
畫面內容如下,有些地方像是名稱、LINE ID 的部份,我們可以先填入假資料,模擬畫面實際有值的樣子:
<template>
<PublicHeader />
<div class="min-h-screen bg-[#fafafa]">
<div class="relative h-48 bg-gradient-to-r from-[#644D5E] to-[#154065] overflow-hidden">
<div class="absolute inset-0 opacity-30">
<div class="animation absolute top-4 left-8 w-3 h-3 bg-white/40 rounded-full"></div>
<div class="animation absolute top-12 right-16 w-2 h-2 bg-white/30 rounded-full"></div>
<div class="animation absolute bottom-8 left-1/4 w-4 h-4 bg-white/20 rounded-full"></div>
<div class="animation absolute top-8 left-1/3 w-2 h-2 bg-white/40 rounded-full"></div>
<div class="animation absolute bottom-12 right-1/3 w-3 h-3 bg-white/30 rounded-full"></div>
</div>
</div>
<div class="relative -mt-16 px-6">
<div class="flex flex-col items-center mb-12">
<div class="relative m-4">
<div class="flex items-center justify-center h-24 w-24 rounded-full border-4 border-white shadow-lg overflow-hidden bg-[#f6f8f9]">
<svg xmlns="http://www.w3.org/2000/svg" class="text-[#E2E8F0]" width="4rem" height="4em" viewBox="0 0 24 24">
<path d="M8.5 13.498l2.5 3.006l3.5-4.506l4.5 6H5m16 1v-14a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z" fill="currentColor" />
</svg>
</div>
<button class="absolute -bottom-1 -right-1 w-8 h-8 bg-[#707070] rounded-full flex items-center justify-center cursor-pointer shadow-md hover:bg-[#525252]">
<svg width="16" height="16" class="text-[#FFFFFF]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"/>
<path d="M12 5v14"/>
</svg>
</button>
</div>
<h1 class="text-2xl font-medium text-[#525252] mb-2">嗨 ooo 你好!</h1>
<p class="text-[#98A0AE] text-sm">LINE ID Uxxxxxxxxxxxxxxxxx</p>
</div>
<div class="max-w-md mx-auto space-y-8">
<div class="space-y-2">
<label class="text-[#98A0AE] text-base font-medium">姓名</label>
<div class="border-b border-[#CCCECF] pb-2">
<input
type="text"
class="w-full border-0 bg-transparent p-0 text-[#525252] placeholder:text-[#98A0AE] focus:outline-none"
/>
</div>
</div>
<div class="space-y-2">
<label class="text-[#525252] text-base font-medium">信箱</label>
<div class="border-b border-[#CCCECF] pb-2">
<input
type="email"
class="w-full border-0 bg-transparent p-0 text-[#525252] placeholder:text-[#98A0AE] focus:outline-none"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
input:focus {
outline: none;
}
.animation {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
</style>
目前會員首頁 UI 的成果如下:
在這個篇章中,我們已經完成註冊流程所需的 UI 製作,包含雙欄式排版、插圖視覺區塊、LINE 註冊按鈕、條款彈窗,以及登入後的會員首頁畫面。透過 Nuxt UI 我們可以快速構建符合品牌風格的元件與佈局,而 SweetAlert 的加入讓互動細節更加完整、生動。
完成這些基礎畫面,我們也能順利往串接 LIFF(LINE Front-end Framework)功能前進囉。在下一篇我們將進一步整合 LIFF 技術,實作 LINE 第三方登入機制,並完成整體註冊流程,讓使用者可以一鍵登入、順利導向會員首頁,敬請期待✨!
⑴ Nuxt UI 參考來源
⑵ Tailwindcss 參考來源
⑶ icones 來源
⑷ sweetalert2 來源