简单页面聊天

news/2025/10/21 20:14:12/文章来源:https://www.cnblogs.com/zhanglijian/p/19156342
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 || 3001app.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) returnif (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) returnsocket.emit('room:join', name, (res) => {if (res && res.ok) {currentRoom.value = namemessages.value = [] // 切换房间清空本地消息}})
}
function leaveRoom () {const name = currentRoom.valueif (!name) returnsocket.emit('room:leave', name, (res) => {if (res && res.ok) {currentRoom.value = nullmessages.value = []}})
}function sendText () {const text = textInput.value.trim()if (!text) returnconst 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) returnconst reader = new FileReader()reader.onload = () => {const payload = {type: 'image',data: reader.result, // data URLname: 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 = trueconsole.log('connected as', username)})socket.on('disconnect', () => {isConnected.value = false})socket.on('chat-message', (msg) => {// 仅追加当前房间或无房间的消息if (msg.room && msg.room !== currentRoom.value) returnappendMessage({ ...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"><inputv-model="roomInput"type="text"placeholder="输入房间名,创建或加入"/><button @click="joinRoom">创建/加入</button><button @click="leaveRoom" :disabled="!currentRoom">退出</button></div><div id="chat-box" class="chat-box"><divv-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"><inputv-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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/942622.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入认识ClassLoader - 一次投产失败的复盘

问题背景 投产日,同事负责的项目新版本发布,版本包是SpringBoot v2.7.18的一个FatJar,java -jar启动报错停止了,输出的异常日志如下: Caused by: org.springframework.beans.factory.BeanCreationException: Erro…

python 包来源镜像

python 镜像python安装包,默认地址非常的慢,可改用国内相关镜像‌清华大学开源软件镜像站‌ 地址:https://pypi.tuna.tsinghua.edu.cn/simple‌阿里云开源镜像站‌ 地址:https://mirrors.aliyun.com/pypi/simple/‌…

CSharp基础复习-1

基本语法 usiing 关键字 using 关键字用于在程序中包含命名空间。一个程序可以包含多个 using 语句 class关键字 class 关键字用于声明一个类。 注释 单行注释 多行注释 成员变量 变量是类的属性或数据成员,用于存储…

软件工程第三次作业-结对作业

软件工程第三次作业——结对作业结对作业 实现一个自动生成小学四则运算题目的命令行程序 (也可以用图像界面,具有相似功能)这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScienc…

AI代码生成技术解析与应用实践

本文详细介绍基于机器学习的代码生成技术,重点分析自然语言转编程代码的工作原理、多语言支持能力、安全过滤机制及实时推荐特性,探讨AI如何提升开发效率并改变编程范式。AWS CodeWhisperer从自然语言创建计算机代码…

米理 课程描述/学习计划/Study program

其实没官方的模板,只是有个类似的东西 https://www.polimi.it/fileadmin/user_upload/futuri_studenti/ammissione-laurea-magistrale/Sample2.pdf

2025年线路调压器厂家推荐榜:10kv线路调压器/单相线路调压器/三相线路调压器/助力电网稳定运行,优选品牌指南

随着电力系统升级、新能源接入规模扩大及电网稳定性要求提升,线路调压器作为关键配电设备,已从传统电力行业扩展至工业、新能源、农村电网等多个领域。2025年,市场需求预计持续增长,但厂商技术实力、产品适配性及服…

2025 智能/商超照明/灯具/灯光/源头厂家推荐榜:上海富明阳凭分区域光效领跑,生鲜 / 百货场景适配优选

在商超竞争聚焦 “商品展示力” 的 2025 年,商超照明已从基础照明升级为 “提升商品吸引力、优化购物体验” 的核心工具。但行业普遍存在光效不均、场景适配差、能耗过高的痛点,优质服务商能精准破解难题。结合光效指…

2025 变电站厂家推荐榜最新资讯:撬装变电站/移动车载变电站/预制舱式变电站/移动变电站/预装式变电站/聚焦智能适配与可靠服务,这家企业成优选​

随着新型电力系统加速建设,新能源并网、电网改造及应急供电等需求持续攀升,变电站作为电力传输核心枢纽,其模块化、智能化与环境适配能力成为选型关键。2025 年全球预制舱箱式变电站市场规模已达 1966 百万美元,市…

银河麒麟Kylin申威SW64系统安装 rpcbind-1.2.5-2.p01.ky10.sw_64.rpm 方法

银河麒麟Kylin申威SW64系统安装 rpcbind-1.2.5-2.p01.ky10.sw_64.rpm 方法​ 一、准备工作​确认系统架构是申威(SW)的​ 一般这个包是专门为申威平台的银河麒麟操作系统(比如 KY10)准备的,你下载的包名里已经有 …

helloworld的输出

helloworld的输出public class hello {public static void main(String[] args){System.out.print("helloword");} }hello类名和文件名hello.java一样 cmd编译Java文件 1、cmd当前Java文件目录 2、javac hel…

2025 艺考文化课推荐榜:济南震华学校 5 星领跑,全阶段体系适配基础补弱到高分冲刺

随着艺考竞争加剧,艺考生对 “文化课精准补弱、高效提分、适配专业课时间” 的需求愈发迫切,专业艺考文化课培训需兼顾 “针对性与系统性”。结合课程完整性、提分效果、师资专业性与用户反馈,2025 年艺考文化课推荐…

2025 广州人力资源/派遣/劳务外包/人事代理/推荐榜:精典人才凭派遣合规 + 全场景适配领跑,企业用工优选

在广州企业用工需求日趋多元化的 2025 年,人力资源与人力资源派遣服务成为企业灵活配置人力、降低用工风险的核心选择。但行业中存在合规性不足、岗位适配差、售后支持弱等痛点,优质服务商可有效规避用工隐患。结合合…

png隐写文件与文件占用

png隐写文件正确解封装 1.ffmpeg自动推测。 2.得到推测格式为png_pipe,尝试使用mpegts格式进行解封装,打开成功并且媒体流大于0则认为成功。 3.使用mpegts上下文替换png上下文。 ps:部分vob文件需以mpeg格式打开。 文件…

Windows和Linux设置Https(SSL)访问 - 详解

Windows和Linux设置Https(SSL)访问 - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "…

题解:P12525 [Aboi Round 1] 私は雨

link 顺带一提这是我第一道没看题解做出来的黑(也是第四道黑)。 写完看了一圈题解,我想说: 欸不是凭啥我不用卡常啊? 前言 这篇题解的复杂度是这样的: 小 \(p\) \(O(q \sqrt n \log \sqrt n + n \sqrt V)\),大 …

完整教程:罗技G102有线鼠标自己维修教程

完整教程:罗技G102有线鼠标自己维修教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "M…

杂谈

代码如下,将这一段代码优化了下 string id = AAA(msg->msg_sender_);if (id.empty()){BBB(msg);VX_INFO_FUN_OUT();return;}#ifdef PROJECT_A//理论上不会到这里,因为id.empty会会处理,这里做一个保护if(U::i…

挖矿-学校挖矿排查

挖矿-学校挖矿排查 1、使用工具分析共有多少IP存在扫描web特征,提交其数量 这里我们直接访问百度网盘将流量下载到本地然后直接导入到 ZUI里面,这个工具很方便对流量进行筛选流量分析工具ZUI安装然后使用命令搜索 co…

读书日记2

四五章深入探讨了软件构建的关键前期工作,让我认识到优秀代码的质量在很大程度上是由设计阶段决定的。 核心收获与深刻见解: 1.设计的层次性思维:McConnell详细阐述了从系统架构到类设计,再到子程序设计的完整层次…