2025-04-09-JavaScript 系列九:第6課 ── 學習用外部狀態管理 vue 元件

2025-04-09-JavaScript 系列九:第6課 ── 學習用外部狀態管理 vue 元件

JavaScript 系列九:第6課 ── 學習用外部狀態管理 vue 元件
課程目標
學習用外部狀態管理 vue 元件
課程內容
前面的課程中,元件的狀態很單純,都是匯入之後直接使用

但是有些元件匯入之後,一開始在畫面上是不顯示的

比方說視窗類型的元件:這種元件需要額外有一個狀態,來管理它是否顯示

課後作業
這一課作業,請先看一下 Dialog 視窗元件的用法

https://quasar.dev/vue-components/dialog

請使用官方的元件試玩工具:Vue SFC Playground

https://sfc.vuejs.org/

請建立 MyDialog.vue 元件,並支援下列功能
v-model 能夠管理視窗是否顯示
預設的 slot 能夠放入視窗內容
完成之後,請在 App.vue 之中匯入此元件,並用多個範例證明,有完成上述規格

做出以上功能,你就完成這次的課程目標了!

好的 👍 我幫你把這一課的作業拆解成「小步驟」來完成,並給你完整範例程式碼。


🎯 學習目標

  • 建立一個 MyDialog.vue 元件
  • 讓它支援 v-model 控制「顯示/隱藏」
  • 允許在 default slot 放入任意內容
  • App.vue 中測試多個 MyDialog

🛠️ Step 1:建立 MyDialog.vue

這個元件要支援 v-model,所以我們要使用 defineProps + defineEmits 搭配 雙向綁定語法

📂 MyDialog.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<template>
<div v-if="modelValue" class="dialog-backdrop">
<div class="dialog">
<!-- slot:顯示外部傳入的內容 -->
<slot></slot>
<div class="actions">
<button @click="closeDialog">關閉</button>
</div>
</div>
</div>
</template>

<script setup>
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})

const emit = defineEmits(['update:modelValue'])

// 關閉對話框
function closeDialog() {
emit('update:modelValue', false)
}
</script>

<style scoped>
.dialog-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
}

.dialog {
background: #fff;
padding: 20px;
border-radius: 12px;
min-width: 300px;
max-width: 500px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}

.actions {
margin-top: 20px;
text-align: right;
}
</style>

🛠️ Step 2:在 App.vue 測試

建立多個 MyDialog,並用不同內容確認 slot 與 v-model 功能。

📂 App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<h2>🎯 MyDialog 測試</h2>

<button @click="showDialog1 = true">開啟對話框1</button>
<button @click="showDialog2 = true">開啟對話框2</button>

<!-- 測試 Dialog 1 -->
<MyDialog v-model="showDialog1">
<h3>對話框 1</h3>
<p>這是第一個對話框的內容。</p>
</MyDialog>

<!-- 測試 Dialog 2 -->
<MyDialog v-model="showDialog2">
<h3>對話框 2</h3>
<p>這裡放的是另一個內容。</p>
<p>slot 讓我們能自由放入 HTML!</p>
</MyDialog>
</template>

<script setup>
import { ref } from 'vue'
import MyDialog from './MyDialog.vue'

const showDialog1 = ref(false)
const showDialog2 = ref(false)
</script>

✅ 功能驗證

  1. 預設不顯示
  2. 點擊按鈕 → 打開對話框
  3. 點擊「關閉」 → v-model 更新,對話框隱藏
  4. slot 的內容可以不同

這樣就完成了 「用外部狀態管理元件」 的作業 🎉


懂了 👍
你不要「測試用的例子」,而是想要一些 實際日常會用到的對話框場景
我幫你設計幾個常見的對話框情境,全部用 MyDialog 來示範:


📂 App.vue(實際範例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<template>
<h2>📌 常見的對話框範例</h2>

<!-- 登入對話框 -->
<button @click="showLogin = true">登入</button>
<MyDialog v-model="showLogin">
<h3>🔑 使用者登入</h3>
<label>
帳號:
<input type="text" placeholder="輸入帳號" />
</label>
<br />
<label>
密碼:
<input type="password" placeholder="輸入密碼" />
</label>
<div class="actions">
<button @click="showLogin = false">取消</button>
<button @click="handleLogin">登入</button>
</div>
</MyDialog>

<!-- 確認刪除對話框 -->
<button @click="showDelete = true">刪除資料</button>
<MyDialog v-model="showDelete">
<h3>⚠️ 確認刪除</h3>
<p>你確定要刪除這筆資料嗎?此動作無法復原。</p>
<div class="actions">
<button @click="showDelete = false">取消</button>
<button class="danger" @click="confirmDelete">確定刪除</button>
</div>
</MyDialog>

<!-- 表單輸入對話框 -->
<button @click="showForm = true">新增聯絡人</button>
<MyDialog v-model="showForm">
<h3>👤 新增聯絡人</h3>
<label>
姓名:
<input type="text" v-model="newContact.name" />
</label>
<br />
<label>
電話:
<input type="text" v-model="newContact.phone" />
</label>
<div class="actions">
<button @click="showForm = false">取消</button>
<button @click="saveContact">儲存</button>
</div>
</MyDialog>
</template>

<script setup>
import { ref } from 'vue'
import MyDialog from './MyDialog.vue'

const showLogin = ref(false)
const showDelete = ref(false)
const showForm = ref(false)

const newContact = ref({ name: '', phone: '' })

function handleLogin() {
alert('登入成功!(這裡可以改成實際 API 呼叫)')
showLogin.value = false
}

function confirmDelete() {
alert('資料已刪除!')
showDelete.value = false
}

function saveContact() {
alert(`已新增聯絡人:${newContact.value.name} / ${newContact.value.phone}`)
newContact.value = { name: '', phone: '' }
showForm.value = false
}
</script>

<style scoped>
.actions {
margin-top: 20px;
text-align: right;
}

button {
margin: 4px;
padding: 6px 12px;
border-radius: 6px;
border: 1px solid #ccc;
cursor: pointer;
}

button.danger {
background: #e74c3c;
color: white;
border: none;
}
</style>

✨ 提供的例子

  1. 登入對話框:帳號 + 密碼輸入
  2. 確認刪除對話框:刪除前提醒
  3. 表單輸入對話框:新增聯絡人

這三種是日常開發中最常用的對話框場景。


好問題 👌
我們之前有把 MyDialog.vue 寫好,但我剛剛只放了 App.vue 的日常例子,沒有再把 MyDialog.vue 重複給你。
我幫你整理完整專案結構,這樣你就清楚知道 MyDialog.vue 要放在哪裡。


📂 專案結構

1
2
3
src/
├─ App.vue ← 使用 MyDialog 的地方
└─ MyDialog.vue ← 對話框元件

📄 MyDialog.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
<div v-if="modelValue" class="dialog-backdrop">
<div class="dialog">
<!-- slot:顯示外部傳入的內容 -->
<slot></slot>
</div>
</div>
</template>

<script setup>
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})

const emit = defineEmits(['update:modelValue'])

// 讓外部可以呼叫:<MyDialog v-model="xxx"> 來控制顯示
function closeDialog() {
emit('update:modelValue', false)
}
</script>

<style scoped>
.dialog-backdrop {
position: fixed;
inset: 0; /* top, right, bottom, left = 0 */
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
}

.dialog {
background: #fff;
padding: 20px;
border-radius: 12px;
min-width: 300px;
max-width: 500px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
</style>

📄 App.vue

(這就是我剛剛給的登入、刪除、表單三個範例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<template>
<h2>📌 常見的對話框範例</h2>

<!-- 登入對話框 -->
<button @click="showLogin = true">登入</button>
<MyDialog v-model="showLogin">
<h3>🔑 使用者登入</h3>
<label>
帳號:
<input type="text" placeholder="輸入帳號" />
</label>
<br />
<label>
密碼:
<input type="password" placeholder="輸入密碼" />
</label>
<div class="actions">
<button @click="showLogin = false">取消</button>
<button @click="handleLogin">登入</button>
</div>
</MyDialog>

<!-- 確認刪除對話框 -->
<button @click="showDelete = true">刪除資料</button>
<MyDialog v-model="showDelete">
<h3>⚠️ 確認刪除</h3>
<p>你確定要刪除這筆資料嗎?此動作無法復原。</p>
<div class="actions">
<button @click="showDelete = false">取消</button>
<button class="danger" @click="confirmDelete">確定刪除</button>
</div>
</MyDialog>

<!-- 表單輸入對話框 -->
<button @click="showForm = true">新增聯絡人</button>
<MyDialog v-model="showForm">
<h3>👤 新增聯絡人</h3>
<label>
姓名:
<input type="text" v-model="newContact.name" />
</label>
<br />
<label>
電話:
<input type="text" v-model="newContact.phone" />
</label>
<div class="actions">
<button @click="showForm = false">取消</button>
<button @click="saveContact">儲存</button>
</div>
</MyDialog>
</template>

<script setup>
import { ref } from 'vue'
import MyDialog from './MyDialog.vue'

const showLogin = ref(false)
const showDelete = ref(false)
const showForm = ref(false)

const newContact = ref({ name: '', phone: '' })

function handleLogin() {
alert('登入成功!(這裡可以改成實際 API 呼叫)')
showLogin.value = false
}

function confirmDelete() {
alert('資料已刪除!')
showDelete.value = false
}

function saveContact() {
alert(`已新增聯絡人:${newContact.value.name} / ${newContact.value.phone}`)
newContact.value = { name: '', phone: '' }
showForm.value = false
}
</script>

<style scoped>
.actions {
margin-top: 20px;
text-align: right;
}

button {
margin: 4px;
padding: 6px 12px;
border-radius: 6px;
border: 1px solid #ccc;
cursor: pointer;
}

button.danger {
background: #e74c3c;
color: white;
border: none;
}
</style>

👉 這樣你有 MyDialog.vue(元件本體)App.vue(日常例子)

要不要我再幫你把 MyDialog.vue 加上「右上角關閉按鈕 (X)」?這樣會更像實際專案常用的 Dialog。
這個問題問得很好 👍
按鈕顏色沒有「絕對規定」,但大部分 UI 設計(像 Material DesignBootstrapAnt DesignQuasar 等框架)都有一些「常見的配色習慣」:


🔑 常見按鈕顏色建議

1. 登入 / 確認 / 儲存

  • 主要動作(Primary Action)

  • 常見顏色:藍色 (Blue)綠色 (Green)

  • 例子:

    • 藍色:#1976d2、#409eff(Element UI)、Bootstrap 的 .btn-primary
    • 綠色:#28a745(Bootstrap Success)

👉 因為「藍色」給人穩定、信任的感覺,
👉 「綠色」則表示成功、完成。


2. 刪除 / 危險動作

  • ⚠️ Danger / Destructive Action

  • 常見顏色:紅色 (Red)

  • 例子:

    • #e74c3c(常見 danger 紅色)
    • Bootstrap .btn-danger
    • Quasar color="negative"

👉 紅色能立即讓使用者警覺「這個動作不可逆」。


3. 新增 / 建立

  • Add / Create

  • 常見顏色:綠色 (Green)

  • 例子:

    • #4caf50(Material Design Green 500)
    • Bootstrap .btn-success

👉 綠色象徵「新增、成長、正向」。


4. 取消 / 關閉

  • Secondary / Neutral

  • 常見顏色:灰色 (Gray)

  • 例子:

    • #6c757d(Bootstrap .btn-secondary
    • Material Design「Cancel」多用灰色文字 + 輕量按鈕

👉 灰色代表「不強調、不會造成危險」。


🎨 小總結表

動作 常見顏色
登入 / 確認 / 儲存 藍色 / 綠色
刪除 紅色
新增 綠色
取消 / 關閉 灰色

要不要我幫你把 App.vue 的範例按鈕,套上這些「符合業界習慣的顏色」?

好的 👍 我幫你把你的 Lesson6.vue 中按鈕顏色修改成 符合業界習慣的配色,並標註用途:


修改後的 Lesson6.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<template>
<h2>JavaScript 系列九:第6課 ── 學習用外部狀態管理 vue 元件</h2>

<!-- 登入對話框 -->
<button class="primary" @click="showLogin = true">登入</button>
<MyDialog v-model="showLogin">
<h3>🔑 使用者登入</h3>
<label>
帳號:
<input type="text" placeholder="輸入帳號" />
</label>
<br />
<label>
密碼:
<input type="password" placeholder="輸入密碼" />
</label>
<div class="actions">
<button class="secondary" @click="showLogin = false">取消</button>
<button class="primary" @click="handleLogin">登入</button>
</div>
</MyDialog>

<!-- 確認刪除對話框 -->
<button class="danger" @click="showDelete = true">刪除資料</button>
<MyDialog v-model="showDelete">
<h3>⚠️ 確認刪除</h3>
<p>你確定要刪除這筆資料嗎?此動作無法復原。</p>
<div class="actions">
<button class="secondary" @click="showDelete = false">取消</button>
<button class="danger" @click="confirmDelete">確定刪除</button>
</div>
</MyDialog>

<!-- 表單輸入對話框 -->
<button class="success" @click="showForm = true">新增聯絡人</button>
<MyDialog v-model="showForm">
<h3>👤 新增聯絡人</h3>
<label>
姓名:
<input type="text" v-model="newContact.name" />
</label>
<br />
<label>
電話:
<input type="text" v-model="newContact.phone" />
</label>
<div class="actions">
<button class="secondary" @click="showForm = false">取消</button>
<button class="success" @click="saveContact">儲存</button>
</div>
</MyDialog>
</template>

<script setup>
import { ref } from 'vue'
import MyDialog from './Lesson6_components/MyDialog.vue'

const showLogin = ref(false)
const showDelete = ref(false)
const showForm = ref(false)

const newContact = ref({ name: '', phone: '' })

function handleLogin() {
alert('登入成功!(這裡可以改成實際 API 呼叫)')
showLogin.value = false
}

function confirmDelete() {
alert('資料已刪除!')
showDelete.value = false
}

function saveContact() {
alert(`已新增聯絡人:${newContact.value.name} / ${newContact.value.phone}`)
newContact.value = { name: '', phone: '' }
showForm.value = false
}
</script>

<style scoped>
.actions {
margin-top: 20px;
text-align: right;
}

button {
margin: 4px;
padding: 6px 12px;
border-radius: 6px;
border: 1px solid #ccc;
cursor: pointer;
}

/* 主要動作:登入 / 儲存 */
button.primary {
background-color: #1976d2; /* 藍色 */
color: white;
border: none;
}

/* 成功 / 新增 */
button.success {
background-color: #28a745; /* 綠色 */
color: white;
border: none;
}

/* 危險 / 刪除 */
button.danger {
background-color: #e74c3c; /* 紅色 */
color: white;
border: none;
}

/* 次要 / 取消 */
button.secondary {
background-color: #6c757d; /* 灰色 */
color: white;
border: none;
}

h2 {
color: rgb(149, 44, 95);
}
</style>

🎨 套用規則說明

  • primary → 藍色:登入 / 確認
  • success → 綠色:新增 / 儲存
  • danger → 紅色:刪除
  • secondary → 灰色:取消