今天將之前所學的,小小結合作個簡易聊天室吧!
開箱25:Vue 3 + Firebase Cloud Firestore 簡單CRUD功能
開箱26:Vue 3 + Firebase Storage存儲服務簡單實作
▲ 成果
Demo網址:https://hahasister-ironman-project.netlify.app/#/chatRoom
☆★☆★ 詳細程式碼 前往 >> 本次程式 commit 紀錄
// 建立主頁
<template>
<div class="container my-3">
<h1 class="text-2xl text-center my-3">vue + firebase 線上即時聊天室</h1>
<p class="text-red-400 text-center">
此功能僅供測試,✪請勿輸入具有攻擊性、不堪入耳的言語✪
</p>
<Login @startChat="startChat" v-if="step === 1" />
<Chat :username="username" v-else />
</div>
</template>
<script setup>
import Login from '../components/ChatRoom/Login.vue';
import Chat from '../components/ChatRoom/Chat.vue';
import { ref } from 'vue';
const step = ref(1);
const username = ref('');
const startChat = (name) => {
step.value = 2;
username.value = name;
};
</script>
//Login.vue
輸入暱稱 進入step2(換成chat.vue)
<template>
<div class="flex flex-col justify-center items-center">
<div class="flex flex-col justify-center mb-4">
<label for="username" class="block mb-4">請輸入暱稱,開始聊天</label>
<input
id="username"
type="text"
class="border block p-2"
v-model.trim="username"
@keyup.enter="$emit('startChat', username)"
/>
</div>
<button
type="button"
class="boder bg-green-400 p-2"
@click="$emit('startChat', username)"
:disabled="!username"
>
開始使用
</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const username = ref('');
</script>
//Chat.vue
<template>
<div class="container min-h-screen relative" ref="messageContent">
<div class="flex flex-wrap items-center p-2 sticky top-0 bg-gray-200">
<label for="fileInput">
<img src="/src/assets/icon/attach-file.png" alt="" />
</label>
<input
type="file"
ref="fileInput"
id="fileInput"
accept="image/*"
@change="handleFileSelect"
class="hidden"
/>
<input
type="text"
class="boder p-1 flex-1"
v-model.trim="message"
@keyup.enter="addMessage"
placeholder="輸入訊息"
/>
<button
class="boder p-2 bg-green-400 text-white"
type="button"
@click="addMessage"
>
送出
</button>
<div v-if="uploadProgress !== null" class="w-full">
<progress :value="uploadProgress" max="100" class="w-[90%]">
{{ uploadProgress }}%
</progress>
{{ uploadProgress }}%
</div>
</div>
<div v-if="isLoading" class="mt-6">Loading...</div>
<div class="mt-6" v-else>
<div
class="flex mb-3 gap-2"
v-for="(item, key) in chatroom"
:key="key"
:class="{
'flex-row-reverse': item.username === username,
}"
>
<div class="avatar mt-1" v-if="item.username !== username">
<span> {{ item.username.slice(0, 1) }}</span>
</div>
<div class="max-w-2/3">
<div class="text-right">
<small class="text-gray-500 ml-2">
{{ new Date(item.time).toLocaleDateString() }}
{{ new Date(item.time).toLocaleTimeString() }}</small
>
</div>
<div
class="p-2 mt-2 rounded-lg"
:class="{
'bg-blue-500 text-white': item.username === username,
'bg-gray-100': item.username !== username,
}"
>
<p v-if="item.type === 'text'">{{ item.message }}</p>
<img
v-else-if="item.type === 'image'"
:src="item.message"
alt="Image"
class="max-w-[150px]"
/>
</div>
</div>
</div>
</div>
</div>
</template>
template 部分
-檔案上傳和訊息輸入區域: 這個部分有一個檔案上傳按鈕、一個文字輸入框和一個「送出」按鈕。
-上傳進度條: 如果有檔案正在上傳,會顯示一個進度條。
-聊天訊息顯示區域: 這裡會列出所有的聊天訊息。訊息可以是文字或圖片。
setup 部分
-引入了 Firebase 的資料庫(Firestore)和儲存(Storage)服務,以及 Vue.js 的一些功能。
-狀態變數: 有一些 ref 用來儲存狀態,像是 isLoading、message、chatroom 等。
-addMessage 函數: 用來將新的訊息加入到 Firestore 的資料庫。
-handleFileSelect 和 uploadImage 函數: 處理檔案上傳的邏輯。
-onMounted 和 onUnmounted 鉤子: 在組件掛載和卸載時,設定和清除 Firestore 的資料監聽。
詳細的方法,基本上都是從這兩篇複製過來的
開箱25:Vue 3 + Firebase Cloud Firestore 簡單CRUD功能
開箱26:Vue 3 + Firebase Storage存儲服務簡單實作
<script setup>
import { db } from '@/services/firebase.js';
import { storage } from '@/services/firebase.js';
import {
ref as storageRef,
uploadBytesResumable,
getDownloadURL,
} from 'firebase/storage';
import {
collection,
addDoc,
onSnapshot,
orderBy,
query,
updateDoc,
doc,
deleteDoc,
} from 'firebase/firestore';
import { onMounted, onUnmounted, ref, inject, nextTick } from 'vue';
const props = defineProps({
username: String,
});
const isLoading = ref(true);
const messageContent = ref(null);
const fileInput = ref(null);
const message = ref('');
const chatroom = ref([]);
const uploadProgress = ref(null);
const scrollTo = inject('scrollTo');
let unsubscribe;
const addMessage = async () => {
if (!message) {
return;
}
try {
const docRef = await addDoc(collection(db, 'messages'), {
message: message.value,
username: props.username,
time: Date.now(),
type: 'text',
});
} catch (e) {
console.error('Error adding document: ', e);
} finally {
message.value = '';
scrollTo();
}
};
const handleFileSelect = (event) => {
const file = event.target.files[0]; // 抓取file
if (file && file.size > 2 * 1024 * 1024) {
alert('文件大小超過2MB,請重新上傳');
fileInput.value.value = '';
return;
}
uploadImage(file);
};
const uploadImage = async (file) => {
const storageName = storageRef(storage, `images/${file.name}`);
const uploadTask = uploadBytesResumable(storageName, file);
uploadTask.on(
'state_changed',
(snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
uploadProgress.value = parseInt(progress);
},
(error) => {
console.error('Upload failed:', error);
},
async () => {
const downloadURL = await getDownloadURL(uploadTask.snapshot.ref);
// 將 downloadURL 存到 Firestore 的訊息中
uploadProgress.value = null;
fileInput.value.value = '';
await addDoc(collection(db, 'messages'), {
message: downloadURL,
username: props.username,
time: Date.now(),
type: 'image',
});
await nextTick();
scrollTo();
}
);
};
onMounted(async () => {
const lastestQuery = query(collection(db, 'messages'), orderBy('time'));
unsubscribe = onSnapshot(
lastestQuery,
(snapshot) => {
chatroom.value = snapshot.docs.map((doc) => {
return {
id: doc.id,
...doc.data(),
};
});
isLoading.value = false;
},
(error) => {
console.error('Error getting documents: ', error);
}
);
});
onUnmounted(() => {
if (unsubscribe) {
unsubscribe();
}
});
</script>
<style lang="scss" scoped>
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #eee;
border: 1px solid;
display: flex;
justify-content: center;
font-size: 20px;
color: #999;
}
</style>
那我們明天再見了~