Vue 3 + Vant 4 企业级聊天界面实战:3步实现消息、图片、表情发送
·
Vue 3 + Vant 4 企业级聊天界面实战:3步实现消息、图片、表情发送
在移动互联网时代,即时通讯功能已成为企业应用不可或缺的一部分。无论是内部协作还是客户服务,一个高效、美观的聊天界面都能显著提升用户体验。本文将带你使用Vue 3和Vant 4组件库,从零构建一个功能完备的企业级聊天界面。
1. 环境准备与项目初始化
首先确保你的开发环境已安装Node.js(建议版本16+)和Vue CLI。我们使用Vite作为构建工具,它能提供更快的开发体验。
npm create vite@latest enterprise-chat --template vue
cd enterprise-chat
npm install vant@next vue-router@4 axios
安装完成后,我们需要配置Vant按需引入。在 vite.config.js 中添加:
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'
export default {
plugins: [
vue(),
Components({
resolvers: [VantResolver()],
}),
],
}
接下来创建基础路由结构。在 src/router/index.js 中:
import { createRouter, createWebHistory } from 'vue-router'
import Chat from '../views/Chat.vue'
const routes = [
{
path: '/chat',
name: 'Chat',
component: Chat,
meta: { title: '企业聊天' }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
2. 核心聊天组件实现
2.1 消息列表渲染
创建 src/components/ChatWindow.vue ,实现消息列表的基础结构:
<template>
<div class="chat-container">
<van-nav-bar
title="企业聊天"
left-arrow
@click-left="$router.go(-1)"
/>
<div class="message-list" ref="messageList">
<div
v-for="(msg, index) in messages"
:key="index"
:class="['message-item', msg.sender === 'me' ? 'my-message' : 'other-message']"
>
<van-image
v-if="msg.type === 'image'"
width="200"
height="200"
:src="msg.content"
@click="previewImage(msg.content)"
/>
<div v-else-if="msg.type === 'text'" class="text-message">
{{ msg.content }}
</div>
<div class="message-time">{{ formatTime(msg.time) }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { ImagePreview } from 'vant'
const messages = ref([
{
type: 'text',
content: '您好,请问有什么可以帮您?',
sender: 'other',
time: new Date()
}
])
const previewImage = (url) => {
ImagePreview([url])
}
const formatTime = (date) => {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
// 自动滚动到底部
const messageList = ref(null)
const scrollToBottom = () => {
nextTick(() => {
if (messageList.value) {
messageList.value.scrollTop = messageList.value.scrollHeight
}
})
}
onMounted(scrollToBottom)
</script>
<style scoped>
.chat-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 15px;
background-color: #f5f5f5;
}
.message-item {
margin-bottom: 15px;
display: flex;
flex-direction: column;
}
.my-message {
align-items: flex-end;
}
.other-message {
align-items: flex-start;
}
.text-message {
max-width: 70%;
padding: 10px 15px;
border-radius: 18px;
word-break: break-word;
}
.my-message .text-message {
background-color: #1989fa;
color: white;
}
.other-message .text-message {
background-color: white;
color: #333;
}
.message-time {
font-size: 12px;
color: #999;
margin-top: 5px;
}
</style>
2.2 输入框与功能切换
在 ChatWindow.vue 中添加输入区域:
<template>
<!-- 原有代码... -->
<div class="input-area">
<van-field
v-model="inputText"
rows="1"
autosize
type="textarea"
placeholder="请输入消息"
@keyup.enter="sendTextMessage"
/>
<div class="action-buttons">
<van-icon
name="smile-o"
size="24"
@click="toggleEmojiPicker"
/>
<van-icon
name="photo-o"
size="24"
@click="handleImageUpload"
/>
<van-button
size="small"
type="primary"
@click="sendTextMessage"
>
发送
</van-button>
</div>
<div v-if="showEmojiPicker" class="emoji-picker">
<span
v-for="(emoji, index) in emojis"
:key="index"
@click="addEmoji(emoji)"
>
{{ emoji }}
</span>
</div>
</div>
</template>
<script setup>
// 新增状态
const inputText = ref('')
const showEmojiPicker = ref(false)
const emojis = ['😀', '😂', '😍', '👍', '❤️', '🙏', '🎉', '🔥']
const toggleEmojiPicker = () => {
showEmojiPicker.value = !showEmojiPicker.value
}
const addEmoji = (emoji) => {
inputText.value += emoji
}
const handleImageUpload = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.onchange = (e) => {
const file = e.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = (event) => {
sendMessage({
type: 'image',
content: event.target.result,
sender: 'me',
time: new Date()
})
}
reader.readAsDataURL(file)
}
}
input.click()
}
const sendTextMessage = () => {
if (inputText.value.trim()) {
sendMessage({
type: 'text',
content: inputText.value,
sender: 'me',
time: new Date()
})
inputText.value = ''
showEmojiPicker.value = false
}
}
const sendMessage = (msg) => {
messages.value.push(msg)
scrollToBottom()
}
</script>
<style scoped>
.input-area {
padding: 10px;
background-color: white;
border-top: 1px solid #eee;
}
.action-buttons {
display: flex;
align-items: center;
padding-top: 8px;
}
.action-buttons > * {
margin-right: 10px;
}
.emoji-picker {
display: flex;
flex-wrap: wrap;
padding: 10px;
max-height: 200px;
overflow-y: auto;
border-top: 1px solid #eee;
}
.emoji-picker span {
font-size: 24px;
margin: 5px;
cursor: pointer;
}
</style>
2.3 移动端适配优化
为了确保在各种移动设备上都有良好的显示效果,我们需要添加一些适配代码:
- 在
main.js中添加视口适配:
import 'amfe-flexible'
- 在
vite.config.js中配置postcss:
import autoprefixer from 'autoprefixer'
import postcssPxToViewport from 'postcss-px-to-viewport'
export default {
css: {
postcss: {
plugins: [
autoprefixer(),
postcssPxToViewport({
viewportWidth: 375,
unitPrecision: 3,
viewportUnit: 'vw',
selectorBlackList: ['.ignore'],
minPixelValue: 1,
mediaQuery: false
})
]
}
}
}
- 添加全局样式
src/styles/global.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
height: 100%;
}
3. 高级功能实现
3.1 消息持久化与本地存储
为了在页面刷新后保留聊天记录,我们可以使用localStorage:
import { watch } from 'vue'
const CHAT_STORAGE_KEY = 'enterprise_chat_messages'
// 从本地存储加载消息
const loadMessages = () => {
const saved = localStorage.getItem(CHAT_STORAGE_KEY)
if (saved) {
messages.value = JSON.parse(saved).map(msg => ({
...msg,
time: new Date(msg.time)
}))
}
}
// 保存消息到本地存储
watch(messages, (newVal) => {
const toSave = newVal.map(msg => ({
...msg,
time: msg.time.getTime()
}))
localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(toSave))
}, { deep: true })
onMounted(loadMessages)
3.2 集成企业微信API
如果需要与企业微信深度集成,可以添加以下功能:
import { onMounted } from 'vue'
const initWeChatSDK = () => {
return new Promise((resolve) => {
// 这里应该是从后端获取签名的API调用
// 实际项目中需要替换为真实的API调用
const config = {
debug: false,
appId: 'YOUR_APP_ID',
timestamp: 0,
nonceStr: '',
signature: '',
jsApiList: [
'previewImage',
'chooseImage',
'uploadImage',
'startRecord',
'stopRecord',
'onVoiceRecordEnd'
]
}
wx.config(config)
wx.ready(() => {
console.log('WeChat SDK ready')
resolve()
})
wx.error((err) => {
console.error('WeChat SDK error:', err)
})
})
}
onMounted(async () => {
if (window.wx) {
await initWeChatSDK()
}
})
3.3 性能优化
对于大型聊天应用,我们需要考虑性能优化:
- 虚拟滚动:当消息数量很多时,使用虚拟滚动技术
npm install vue-virtual-scroller
- 在
main.js中引入:
import VueVirtualScroller from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
app.use(VueVirtualScroller)
- 修改消息列表:
<template>
<RecycleScroller
class="message-list"
:items="messages"
:item-size="80"
key-field="id"
v-slot="{ item }"
>
<div :class="['message-item', item.sender === 'me' ? 'my-message' : 'other-message']">
<!-- 消息内容保持不变 -->
</div>
</RecycleScroller>
</template>
4. 测试与部署
4.1 单元测试
添加Jest进行单元测试:
npm install --save-dev jest @vue/test-utils @vue/vue3-jest babel-jest
创建 jest.config.js :
module.exports = {
moduleFileExtensions: ['js', 'json', 'vue'],
transform: {
'^.+\\.js$': 'babel-jest',
'^.+\\.vue$': '@vue/vue3-jest'
},
testEnvironment: 'jsdom'
}
编写一个简单的测试用例 tests/chat.spec.js :
import { mount } from '@vue/test-utils'
import ChatWindow from '../src/components/ChatWindow.vue'
describe('ChatWindow', () => {
it('renders initial message', () => {
const wrapper = mount(ChatWindow)
expect(wrapper.text()).toContain('您好,请问有什么可以帮您?')
})
it('sends new message', async () => {
const wrapper = mount(ChatWindow)
await wrapper.find('textarea').setValue('Hello')
await wrapper.find('.van-button').trigger('click')
expect(wrapper.text()).toContain('Hello')
})
})
4.2 部署建议
对于企业级应用,建议使用以下部署方案:
- Docker容器化 :
FROM node:16 as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
- Nginx配置 :
server {
listen 80;
server_name yourdomain.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
}
}
- CI/CD集成 :
name: CI/CD Pipeline
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Docker build
run: docker build -t your-image-name .
- name: Login to Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Push to Docker Hub
run: docker push your-image-name
通过以上步骤,我们已经完成了一个功能完备的企业级聊天界面实现。这个方案不仅包含了基础的聊天功能,还考虑了企业应用特有的需求,如与企业微信的集成、移动端适配、性能优化等。
更多推荐

所有评论(0)