iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0
Vue.js

Vue3歡樂套件箱耶系列 第 29

開箱29:一秒變即時聊天室~Vue3+Firebase簡易實作

  • 分享至 

  • xImage
  •  

今天將之前所學的,小小結合作個簡易聊天室吧!

開箱25:Vue 3 + Firebase Cloud Firestore 簡單CRUD功能
開箱26:Vue 3 + Firebase Storage存儲服務簡單實作

https://ithelp.ithome.com.tw/upload/images/20231014/20142016t1FhnNUplq.png
▲ 成果

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>

那我們明天再見了~


上一篇
開箱28:新手搭建~Vue+Vite+GitHub部署到Firebase Hosting
下一篇
><點閱率太差了!fine~以「收穫心態」來回顧吧!
系列文
Vue3歡樂套件箱耶30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言