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 移动端适配优化

为了确保在各种移动设备上都有良好的显示效果,我们需要添加一些适配代码:

  1. main.js 中添加视口适配:
import 'amfe-flexible'
  1. 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
        })
      ]
    }
  }
}
  1. 添加全局样式 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 性能优化

对于大型聊天应用,我们需要考虑性能优化:

  1. 虚拟滚动:当消息数量很多时,使用虚拟滚动技术
npm install vue-virtual-scroller
  1. main.js 中引入:
import VueVirtualScroller from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

app.use(VueVirtualScroller)
  1. 修改消息列表:
<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 部署建议

对于企业级应用,建议使用以下部署方案:

  1. 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;"]
  1. 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;
  }
}
  1. 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

通过以上步骤,我们已经完成了一个功能完备的企业级聊天界面实现。这个方案不仅包含了基础的聊天功能,还考虑了企业应用特有的需求,如与企业微信的集成、移动端适配、性能优化等。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐