前言
工作室的二手市场项目需要新加一个发起聊天功能,但是现在还没开始,我先做一个玩一下,所以做的比较简单,数据那些也都是自己随便写了一下,但是功能应该是到位了,数据的话到时候后端写好接口我再调就是了替换一下即可
成品 And 解析
先看看成品叭,这里就很简单的制作了一个登录,也不能算登录吧,反正也设置了两个用户,登录的功能我这里就懒得写了,主打一个从简(大佬也可以再写个login页面自己加token登录啥的)
然后任意选择一个用户进入聊天,其实这里按道理来说在QQ中应该是一个很多对话列表的页面,接着自己选择一个进入聊天, 这个对话列表也只需要再写个页面用<li>渲染出来点击之后给个params传对话的对象,跳到最后的聊天界面就行(如果要我做我会这么做,但是工作室不需要这个功能所以我也偷懒了~)
这里以懒大王的用户名进入聊天,咱一个一个部分解析,先头部,再聊天框,最后底部(表情也放这里讲好了)
Header头部栏
Header头部其实没什么东西所以我写的也很简单,我写到了Components文件夹下,就是一个返回路由back,然后一个聊天名字,不过这个聊天名字我是用pinia来存储的,也算一个组件传值吧,而且也存到了localstorage里面,不然刷新之后就会丢
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { useStore } from '../stores/index'
const store = useStore()
const router = useRouter()
const back = () => {
router.back()
}
</script>
<template>
<div class="head">
<van-icon name="arrow-left" size="20px" id="icon" @click="back" />
<div class="text">{{ store.getGpt() }}</div>
</div>
</template>
聊天记录部分
chatContainer就是整个聊天界面哈,我给了设了个 ref 值,可以看到进入时会自动拉到聊天记录最下面,发消息的时候也会自动回到最下面,因为我在Onmouted和Onupdated中都设置了相应方法
onUpdated(() => {
// 在视图更新之后执行滚动逻辑
if (chatContainer.value) {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight //scrollHeight表示可滚动内容的整体高度。scrollTop是可滚动元素的一个属性,用于设置滚动条的垂直偏移量
}
})
onMounted(()=>{
if (chatContainer.value) {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight //初始加载时也滚动一下
}
messageItem().forEach((item)=>{
messageList.push(item)
})
})
具体的消息列表我写了一些数据来测,也可以说是来渲染吧,主要还是写点数据方便我看着写样式哈哈
这里需要特别注意一下:avatar这个头像属性我原本是只写了一个字符串但是无法生效,之前我的写法是 avatar:'~/assets/1.jpg' ,但是是不对的怎么都弄不出来(这个~就是大家熟知的src哈,只是别人都写成@,我换个不一样的)
export const messageItem = () => { //怎么存的话就随便可以localhost,如果是完整的项目可以从后端调接口,后端会有存聊天记录的数据库
return [
{
name: "小鳄鱼", //姓名
content: "你叫什么?",
time: "2023-10-22 19:55:12",
avatar: new URL('~/assets/1.jpg', import.meta.url).href,
},
{
name: "懒大王", //姓名
content: "我没叫啊",
time: "2023-10-22 19:56:34",
avatar: new URL('~/assets/2.jpg', import.meta.url).href,
},
{
name: "小鳄鱼", //姓名
content: "我说你叫什么?",
time: "2023-10-22 19:57:12",
avatar: new URL('~/assets/1.jpg', import.meta.url).href,
},
{
name: "懒大王", //姓名
content: "我根本没说话啊",
time: "2023-10-22 19:58:34",
avatar: new URL('~/assets/2.jpg', import.meta.url).href,
},
{
name: "懒大王", //姓名
content: "😮😮😮",
time: "2023-10-22 19:58:54",
avatar: new URL('~/assets/2.jpg', import.meta.url).href,
},
{
name: "小鳄鱼", //姓名
content: "6",
time: "2023-10-22 19:59:12",
avatar: new URL('~/assets/1.jpg', import.meta.url).href,
},
{
name: "小鳄鱼", //姓名
content: "😅",
time: "2023-10-22 20:00:10",
avatar: new URL('~/assets/1.jpg', import.meta.url).href,
},
]
}
<div v-for="item in messageList" :key="item.time">
<template v-if="item.name !== store.getGpt()">
<div class="content-right">
<span class="time">{{ item.time }}</span>
<span class="name">{{ item.name }}</span>
<img class="avatar" :src=item.avatar alt="头像">
</div>
<div class="contentbox-right">
<span class="content">
{{ item.content }}
</span>
</div>
</template>
<template v-else>
<div class="content-left">
<img class="avatar" :src=item.avatar alt="头像">
<span class="name">{{ item.name }}</span>
<span class="time">{{ item.time }}</span>
</div>
<div class="contentbox-left">
<span class="content">
{{ item.content }}
</span>
</div>
</template>
</div>
这里需要注意的就是如果是本人发送的聊天记录需要靠右,对方发送的需要靠左,所以要加个<v-if> 的判断,同时样式也需要有一丢丢的不一样,但是也还好
底部footer输入框
这个地方难搞的点在于,我用element-ui引入的输入框虽然会自动调整自己的高度,但是他那外面一圈的浅灰色背景也需要同时调整自己的高度,而且表情框距离背景的高度也要保持一致(可恶~😭)
很显然,每次内容多到撑起一行,那我们就需要新增这一行的高度,比如说撑起的一行高度是24px,那么我们就需要浅灰色背景新增24px,且!!!聊天记录框也得往上稍稍
思路达成,行动开始。
首先测出每行的高度大概就是24px,就是这个字体大小的高度加上什么乱七八糟的行距,反正就是多一行就多24px,然后我们获取高度scrollHeight,就是这个输入框的高,相除我们就得到了行数,那么行数改变,其他的就跟着变,代码如下:
<template>
<div class="chatroom">
<Chatheader />
<div class="chat-content" ref="chatContainer" @click="emojiHide"
:style="{ maxHeight: `calc(93vh - 100px - ${inputHeight}px)` }">
<!-- 聊天记录 -->
<div v-for="item in messageList" :key="item.time">
<template v-if="item.name !== store.getGpt()">
<div class="content-right">
<span class="time">{{ item.time }}</span>
<span class="name">{{ item.name }}</span>
<img class="avatar" :src=item.avatar alt="头像">
</div>
<div class="contentbox-right">
<span class="content">
{{ item.content }}
</span>
</div>
</template>
<template v-else>
<div class="content-left">
<img class="avatar" :src=item.avatar alt="头像">
<span class="name">{{ item.name }}</span>
<span class="time">{{ item.time }}</span>
</div>
<div class="contentbox-left">
<span class="content">
{{ item.content }}
</span>
</div>
</template>
</div>
</div>
<div class="footer" :style="{ height: `calc(60px + ${inputHeight}px)` }">
<!-- 表情输入 -->
<div class="home-tool">
<van-button type="default" class="emojibtn" @click="emojiShow"><van-icon name="smile-o"
size="30px" /></van-button>
<div class="emoji-container" v-show="emojihowVisible" @click="emojiHide"
:style="{ bottom: `calc(80px + ${emojiHeight}px)` }">
<Emoji @click.stop="chooseEmoji"></Emoji>
</div>
</div>
<!-- 输入框及发送 -->
<van-field v-model="message" rows="1" autosize type="textarea" placeholder="请输入..." right-icon="photo-o" class="box"
@input="handleInput" @focus="emojiHide" />
<van-button class="btn" type="primary" :disabled="message.length === 0" @click="send">发送</van-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed , reactive , onMounted, onUpdated , nextTick } from 'vue'
import Chatheader from '~/components/Chatheader.vue'
import Emoji from '~/components/Emoji.vue'
import { messageItem, getAvatar, formatDate} from '~/utils/message'
import { messages } from '~/types'
import { useStore } from '~/stores/index'
const store = useStore()
// 输入框内容
const message = ref('');
// 消息列表
const messageList:Array<messages> = reactive([])
// 输入框每行的高度
const lineHeight = 24;
// 输入框行数
const lineCount = ref(0)
// 获取输入框元素
const inputField = ref<HTMLInputElement | null>(null);
/**
* 表情面板显示与隐藏
*/
const emojihowVisible = ref(false)
const emojiShow = () => {
emojihowVisible.value = !emojihowVisible.value
}
// 反正就是点到除表情框之外的地方我都给你隐藏
const emojiHide = () => {
emojihowVisible.value = false
}
const chooseEmoji = (e:any) => {
if(e.target.textContent.length <= 2){ //部分表情只占一位,同时如果点到旁边的一点区域会输出全部的表情有点bug
handleInput()
message.value = message.value + e.target.textContent
}
}
/**
* 输入框高度自适应
*/
const inputHeight = computed(() => {
return lineCount.value <= 1 ? 0 : lineHeight * (lineCount.value - 1); // 计算输入框应该有的高度
});
//emojiHeight随行数增多而变化,但是只会增加 24 的一半
const emojiHeight = computed(() => {
return lineCount.value <= 1 ? 0 : (lineHeight-12) * (lineCount.value - 1); // 计算输入框应该有的高度
});
const handleInput = () => {
lineCount.value = inputField.value?.scrollHeight as number / 24;
}
/**
* 自动滚动逻辑
*/
const chatContainer = ref<HTMLElement | null>(null)
onUpdated(() => {
// 在视图更新之后执行滚动逻辑
if (chatContainer.value) {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight //scrollHeight表示可滚动内容的整体高度。scrollTop是可滚动元素的一个属性,用于设置滚动条的垂直偏移量
}
if (inputField.value) {
// console.log("我监听到了");
handleInput()
}
})
const send = () => {
//收回表情框
emojihowVisible.value = false
lineCount.value = 0
const item = {
name: store.getMaster() as string,
content: message.value,
time: formatDate(),
avatar: getAvatar(store.getMaster() as string)
}
messageList.push(item)
message.value = ''
}
onMounted(()=>{
if (chatContainer.value) {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight //初始加载时也滚动一下
}
messageItem().forEach((item)=>{
messageList.push(item)
})
})
onMounted(() => {
nextTick(() => {
const myInput: HTMLInputElement | null = document.querySelector('#van-field-1-input')
inputField.value = myInput
})
})
</script>
表情
表情的话我是直接从网上复制粘贴下来,没想到真能用,类型是string~ ,把他们放到一个数组里,渲染到表情框里就好了,再添加一个点击事件就大功告成
<script lang="ts" setup>
import { emojis } from '~/utils/message'
const emojiArr: Array<string> = emojis().split(',')
</script>
<template>
<div class="emoji">
<ul class="emoji-default">
<li v-for="(item, index) in emojiArr" :key="index">
{{ item }}
</li>
</ul>
</div>
</template>
export const emojis = () => {
return "😀,😁,😂,😃,😄,😅,😆,😉,😊,😋,😎,😍,😘,😗,😙,😚,😇,😐,😑,😶,😏,😣,😥,😮,😯,😪,😫,😴,😌,😛,😜,😝,😒,😓,😔,😕,😲,😷,😖,😞,😟,😤,😢,😭,😦,😧,😨,😬,😰,😱,😳,😵,😡,😠,💘,❤,💓,💔,💕,💖,💗,💙,💚,💛,💜,💝,💞,💟,❣,💪,👈,👉,☝,👆,👇,✌,✋,👌,👍,👎,✊,👊,👋,👏,👐,✍,🍇,🍈,🍉,🍊,🍋,🍌,🍍,🍎,🍏,🍐,🍑,🍒,🍓,🍅,🍆,🌽,🍄,🌰,🍞,🍖,🍗,🍔,🍟,🍕,🍳,🍲,🍱,🍘,🍙,🍚,🍛,🍜,🍝,🍠,🍢,🍣,🍤,🍥,🍡,🍦,🍧,🍨,🍩,🍪,🎂,🍰,🍫,🍬,🍭,🍮,🍯,🍼,☕,🍵,🍶,🍷,🍸,🍹,🍺,🍻,🍴,🌹,🍀,🍎,💰,📱,🌙,🍁,🍂,🍃,🌷,💎,🔪,🔫,🏀,⚽,⚡,👄,👍,🔥,🙈,🙉,🙊,🐵,🐒,🐶,🐕,🐩,🐺,🐱,😺,😸,😹,😻,😼,😽,🙀,😿,😾,🐈,🐯,🐅,🐆,🐴,🐎,🐮,🐂,🐃,🐄,🐷,🐖,🐗,🐽,🐏,🐑,🐐,🐪,🐫,🐘,🐭,🐁,🐀,🐹,🐰,🐇,🐻,🐨,🐼,🐾,🐔,🐓,🐣,🐤,🐥,🐦,🐧,🐸,🐊,🐢,🐍,🐲,🐉,🐳,🐋,🐬,🐟,🐠,🐡,🐙,🐚,🐌,🐛,🐜,🐝,🐞,🦋,😈,👿,👹,👺,💀,☠,👻,👽,👾,💣"
}
学到的东西
差不多了,大概就是这些了,样式这些我就没写出来了,当然这个项目消息还是能正常发送,时间也能正常获取到发送的那个时间哈,我就不演示了,感兴趣的话可以从gitee把这个代码考下来再自己设计其他的,还可以增加聊天背景图片,我原本想的可是没有需求我也就懒得搞了,图片发送功能我没做
- 学到了vite中src的配置
- 头像/图片不能正常引入,如何引入
- pinia的使用
- 聊天中自动高度的设计
项目gitee代码: https://gitee.com/hzhfsa/homemade-mobile-chatfront/tree/master