简单页面聊天

import express from 'express'
import http from 'http'
import { Server } from 'socket.io'
import cors from 'cors'

const app = express()
const PORT = process.env.PORT || 3001

app.use(cors({ origin: ['http://localhost:5173'], credentials: true }))
app.get('/health', (_req, res) => res.json({ ok: true }))

const server = http.createServer(app)
const io = new Server(server, {
  cors: {
    origin: ['http://localhost:5173'],
    methods: ['GET', 'POST']
  }
})

io.on('connection', (socket) => {
  console.log('client connected:', socket.id)

  // 加入房间(创建房间即加入新房间)
  socket.on('room:join', (room, ack) => {
    if (!room || typeof room !== 'string') return ack && ack({ ok: false })
    socket.join(room)
    ack && ack({ ok: true, room })
  })

  // 退出房间
  socket.on('room:leave', (room, ack) => {
    if (!room || typeof room !== 'string') return ack && ack({ ok: false })
    socket.leave(room)
    ack && ack({ ok: true, room })
  })

  // 按房间广播消息;没有房间则广播给其他所有客户端
  socket.on('chat-message', (msg) => {
    if (!msg || !msg.type) return
    if (msg.room) {
      socket.to(msg.room).emit('chat-message', msg)
    } else {
      socket.broadcast.emit('chat-message', msg)
    }
  })

  socket.on('disconnect', () => {
    console.log('client disconnected:', socket.id)
  })
})

server.listen(PORT, () => {
  console.log(`Socket.IO server listening on http://localhost:${PORT}`)
})
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { socket } from '../socket.js'

const messages = ref([])
const textInput = ref('')
const username = `用户-${Math.floor(Math.random() * 1000)}`
const isConnected = ref(false)
// 房间相关
const roomInput = ref('')
const currentRoom = ref(null)

function joinRoom () {
  const name = roomInput.value.trim()
  if (!name) return
  socket.emit('room:join', name, (res) => {
    if (res && res.ok) {
      currentRoom.value = name
      messages.value = [] // 切换房间清空本地消息
    }
  })
}
function leaveRoom () {
  const name = currentRoom.value
  if (!name) return
  socket.emit('room:leave', name, (res) => {
    if (res && res.ok) {
      currentRoom.value = null
      messages.value = []
    }
  })
}

function sendText () {
  const text = textInput.value.trim()
  if (!text) return
  const payload = {
    type: 'text',
    text,
    from: username,
    time: Date.now(),
    room: currentRoom.value || null
  }
  socket.emit('chat-message', payload)
  appendMessage({ ...payload, self: true })
  textInput.value = ''
}

function handleFileChange (e) {
  const file = e.target.files?.[0]
  if (!file) return
  const reader = new FileReader()
  reader.onload = () => {
    const payload = {
      type: 'image',
      data: reader.result, // data URL
      name: file.name,
      from: username,
      time: Date.now(),
      room: currentRoom.value || null
    }
    socket.emit('chat-message', payload)
    appendMessage({ ...payload, self: true })
    e.target.value = ''
  }
  reader.readAsDataURL(file)
}

function appendMessage (msg) {
  messages.value.push(msg)
  const box = document.getElementById('chat-box')
  if (box) box.scrollTop = box.scrollHeight
}

onMounted(() => {
  socket.on('connect', () => {
    isConnected.value = true
    console.log('connected as', username)
  })
  socket.on('disconnect', () => {
    isConnected.value = false
  })
  socket.on('chat-message', (msg) => {
    // 仅追加当前房间或无房间的消息
    if (msg.room && msg.room !== currentRoom.value) return
    appendMessage({ ...msg, self: false })
  })
})

onBeforeUnmount(() => {
  socket.off('chat-message')
  socket.off('disconnect')
  socket.off('connect')
})
</script>

<template>
  <div class="chat">
    <header class="chat-header">
      <span class="status" :class="isConnected ? 'online' : 'offline'"></span>
      在线聊天 - {{ username }}
      <span v-if="currentRoom" class="room-tag">房间:{{ currentRoom }}</span>
    </header>

    <div class="room-bar">
      <input
        v-model="roomInput"
        type="text"
        placeholder="输入房间名,创建或加入"
      />
      <button @click="joinRoom">创建/加入</button>
      <button @click="leaveRoom" :disabled="!currentRoom">退出</button>
    </div>

    <div id="chat-box" class="chat-box">
      <div
        v-for="(m, idx) in messages"
        :key="idx"
        class="msg"
        :class="m.self ? 'self' : 'peer'"
      >
        <div class="meta">
          <span class="from">{{ m.self ? '我' : m.from }}</span>
          <span class="time">{{ new Date(m.time).toLocaleTimeString() }}</span>
        </div>
        <div class="body" v-if="m.type === 'text'">{{ m.text }}</div>
        <div class="body" v-else>
          <img :src="m.data" :alt="m.name" class="image" />
        </div>
      </div>
    </div>

    <div class="chat-input">
      <input
        v-model="textInput"
        type="text"
        :placeholder="currentRoom ? '发消息到房间...' : '未加入房间,消息将广播'"
        @keydown.enter="sendText"
      />
      <button @click="sendText">发送</button>
      <label class="upload">
        图片
        <input type="file" accept="image/*" @change="handleFileChange" />
      </label>
    </div>
  </div>
</template>

<style scoped>
.chat {
  display: flex;
  flex-direction: column;
  height: 80vh; /* 上下缩短 */
  max-width: 1100px; /* 两边加宽 */
  margin: 0 auto;
}
/* 头部美化与在线状态指示 */
.chat-header {
  padding: 8px 16px; /* 上下更紧凑 */
  border-bottom: 1px solid #e5e5e5;
  font-weight: 600;
  background: linear-gradient(90deg, #f8fafc 0%, #eef2ff 100%);
  display: flex;
  align-items: center;
  gap: 8px;
}
.room-tag {
  margin-left: auto;
  font-size: 12px;
  color: #64748b;
}
.status {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  box-shadow: 0 0 0 2px rgba(0,0,0,0.05) inset;
}
.status.online { background: #22c55e; }
.status.offline { background: #ef4444; }

.room-bar {
  display: flex;
  gap: 8px;
  padding: 8px 16px; /* 上下更紧凑 */
  border-bottom: 1px solid #e5e5e5;
  background: #f9fafb;
}
.room-bar input[type="text"] {
  flex: 1;
  padding: 6px 10px;
  border: 1px solid #d1d5db;
  border-radius: 6px;
}
.room-bar button {
  padding: 6px 10px;
  border-radius: 6px;
  border: none;
  background: #6366f1;
  color: #fff;
}
.room-bar button:disabled { opacity: 0.5; cursor: not-allowed; }

.chat-box {
  flex: 1;
  overflow-y: auto;
  padding: 8px 20px; /* 两边加宽,上下稍缩短 */
  background: #f6f7fb;
}
.msg { margin-bottom: 12px; max-width: 70%; }
.msg.self { margin-left: auto; text-align: right; }
.msg.peer { margin-right: auto; }
.meta { font-size: 12px; color: #7a7a7a; margin-bottom: 4px; }
/* 区分发送端与接收端的气泡色彩与边框 */
.body { background: #fff; padding: 8px 10px; border-radius: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.06); border: 1px solid #eaeaea; }
.msg.self .body { background: #e3f2fd; }
.msg.peer .body { background: #ffffff; }
.image { max-width: 360px; border-radius: 6px; }

.chat-input { display: flex; gap: 8px; padding: 8px 16px; border-top: 1px solid #e5e5e5; }
.chat-input input[type="text"] { flex: 1; padding: 8px 10px; border: 1px solid #ccc; border-radius: 6px; }
.chat-input button { padding: 8px 12px; border-radius: 6px; border: none; background: #3b82f6; color: #fff; cursor: pointer; }
.upload { position: relative; overflow: hidden; display: inline-block; padding: 6px 10px; border: 1px solid #ccc; border-radius: 6px; background: #f3f4f6; cursor: pointer; }
.upload input[type="file"] { position: absolute; left: 0; top: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
</style>
posted @ 2025-10-21 20:12  vivi_vimi  阅读(1)  评论(0)    收藏  举报