使用node+prisma+socket+vue3实现一个群聊功能,拓展功能:使用lottie实现入场动画

news2024/11/26 4:48:12

使用node+prisma和vue3实现一个群聊功能

后端代码编写

node环境初始化

新建一个空文件夹node,初始化node环境

npm init -y

修改 packages.json,添加 type 为 module,删除 main

{
  "name": "node",
  "version": "1.0.0",
  "description": "",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

ts环境初始化

tsc --init

如果没有tsc命令,需要提前在全局安装 typescript

npm install typescript ts-node -g

已经安装过的可忽略

检查ts是否安装成功

tsc -v

image-20240927113758648

执行完 tsc --init 会自动生成 tsconfig.json,修改里面的三个配置

"experimentalDecorators": true,                   
"emitDecoratorMetadata": true,  
"strict": false,  
  1. “experimentalDecorators”: true
    • 含义: 启用对装饰器的实验性支持。装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、访问符、属性或参数上。装饰器在 TypeScript 中是实验性的特性,这意味着它们可能在未来的版本中发生变化。
    • 用途: 如果你使用装饰器(例如 Angular 的装饰器),你需要启用这个选项。
  2. “emitDecoratorMetadata”: true
    • 含义: 启用装饰器元数据的生成。装饰器元数据是编译器在装饰器上附加的额外信息,这些信息可以在运行时通过反射 API 访问。
    • 用途: 如果你需要使用反射 API 来访问装饰器元数据,你需要启用这个选项。
  3. “strict”: false
    • 含义: 禁用所有严格类型检查选项。严格模式会启用一系列额外的类型检查规则,这些规则有助于捕获潜在的错误,但有时也会导致一些合法的代码无法通过检查。

依赖安装

在 package.json 添加如下依赖

{
  "scripts": {
    "start": "ts-node ./index.ts"
  },
  "dependencies": {
    "@prisma/client": "^5.19.1",
    "@types/express": "^4.17.21",
    "@types/node": "^22.5.5",
    "axios": "^1.7.7",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.14.1",
    "express": "^4.21.0",
    "inversify": "^6.0.2",
    "inversify-express-utils": "^6.4.6",
    "reflect-metadata": "^0.2.2",
    "socket.io": "^4.8.0",
    "socket.io-client": "^4.8.0",
    "ts-node": "^10.9.2"
  }
}

然后执行 npm install 安装

引入Prisma

首先需要全局安装 prisma

npm install -g prisma

Prisma 官网 https://www.prisma.io/docs

然后再项目根目录执行下面的命令初始化一个基于mysql的项目

prisma init --datasource-provider mysql

此时的项目目录结构如下

image-20241008132442011

在 prisma/schema.prisma 文件中编写表结构

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

// 用户表
model User {
  id       Int    @id @default(autoincrement())
  userName String
}

// 消息表
model Message {
  id         Int      @id @default(autoincrement())
  text       String
  userId     Int
  createTime DateTime @default(now())
  userName   String
}

然后修改 .env 文件的数据库连接地址

image-20241008132827571

DATABASE_URL="mysql://root:abc123@localhost:3306/hichat"

然后执行下面命令执行SQL语句创建表

prisma migrate dev

执行完成后,打开数据库就会发现自动帮我们创建好了表

image-20241008133112553

编写DB

db/index.ts

用于全局共用一个PrismaClient实例

import { injectable, inject } from "inversify";
import { PrismaClient } from "@prisma/client";

@injectable()
export class PrismaDB {
  prisma: PrismaClient;

  // 自动注入
  constructor(@inject("PrismaClient") prisma: () => PrismaClient) {
    this.prisma = prisma();
  }
}

统一结果返回类

utils/Result.ts

export default class Result<T> {
    code: number | 200;
    data: T | null;
    msg: string;

    constructor(code: number | 200, data: T | null, msg?: string) {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }

    static ok<T>(data?: T | null): Result<T> {
        return new Result<T>(200, data, "成功");
    }

    static err<T>(data?: T | null): Result<T> {
        return new Result<T>(500, data, "失败");
    }

    public setCode(code: number | 200) {
        this.code = code;
        return this;
    }

    public setData(data: T | null) {
        this.data = data;
        return this;
    }

    public setMsg(msg: string) {
        this.msg = msg;
        return this;
    }
}

User模块业务实现

service层

src/user/service.ts

import {injectable,inject} from "inversify"
import { PrismaDB } from "../../db"

@injectable()
export class UserService {
   // 自动注入prisma
   constructor(@inject(PrismaDB) private PrismaDB: PrismaDB) {}

   /**
    * 注册新用户
    */
   public async add(userName:string) {
     return await this.PrismaDB.prisma.user.create({
        data:{
           userName:userName
        }
     })
   }

   /**
    * 根据userName查询用户
    */
   public async getUserByUserName(userName:string) {
     return await this.PrismaDB.prisma.user.findFirst({
        where:{
           userName:userName
        }
     })
   }

   /**
    * 查询用户数量
    */
   public async getUserCount() {
     return await this.PrismaDB.prisma.user.count()
   }
}
controller层

src/user/controller.ts

import { inject } from "inversify";
import { controller, httpGet, httpPost } from "inversify-express-utils";
import { UserService } from "./service";
import { Request, Response } from "express";
import Result from "../../utils/Result";

@controller("/user")
export class UserController {
  constructor(@inject(UserService) private readonly server: UserService) {}

  /**
   * 添加用户
   * @param req 
   * @param res 
   */
  @httpPost("/add")
  public async addUser(req: Request, res: Response) {
    let { userName } = req.body;
    let result = await this.server.add(userName);
    res.send(Result.ok(result));
  }

  /**
   * 根据用户名获取用户
   */
  @httpGet("/getUser")
  public async getUser(req: Request, res: Response) {
    let { userName } = req.query;
    let result = await this.server.getUserByUserName(userName as string);
    res.send(Result.ok(result));
  }

  /**
   * 查询用户数量
   */
  @httpGet("/getUserCount")
  public async getUserCount(req: Request, res: Response) {
    let result = await this.server.getUserCount();
    res.send(Result.ok(result));
  }
}

Message模块业务实现

dto层

src/message/message.dto.ts

export class MessageDto {
  id?: number;
  text: string;
  userId: number;
  userName: string;
  createTime?: Date;
}
server层

src/message/service.ts

import { injectable, inject } from "inversify";
import { PrismaDB } from "../../db";
import { MessageDto } from "./message.dto";

@injectable()
export class MessageService {
  // 自动注入prisma
  constructor(@inject(PrismaDB) private PrismaDB: PrismaDB) {}

  /**
   * 添加消息
   */
  public async send(message: MessageDto) {
    return await this.PrismaDB.prisma.message.create({
      data: {
        text: message.text,
        userId: message.userId,
        userName: message.userName,
      },
    });
  }

  /**
   * 获取消息列表
   */
  public async list() {
    return await this.PrismaDB.prisma.message.findMany();
  }
}
controller层

src/message/controller.ts

import { inject } from "inversify";
import { controller, httpGet, httpPost } from "inversify-express-utils";
import { Request, Response } from "express";
import Result from "../../utils/Result";
import { MessageService } from "./service";
import { MessageDto } from "./message.dto";

@controller("/message")
export class MessageController {
  constructor(@inject(MessageService) private server: MessageService) {}

  @httpPost("/send")
  async send(req: Request, res: Response) {
    const message = req.body as MessageDto;
    const result = await this.server.send(message);
    res.json(Result.ok(result));
  }

  @httpGet("/list")
  async list(req: Request, res: Response) {
    const result = await this.server.list();
    res.json(Result.ok(result));
  }
}
socket消息处理

src/message/sockit.ts

import { Server } from "socket.io";
import axios from "axios";
import { createServer } from "http";

// 基础URL
axios.defaults.baseURL = "http://localhost:3000";
// 响应拦截器
axios.interceptors.response.use((res) => {
  return res.data;
});

// 创建一个通信服务器
const httpServer = createServer();
const io = new Server(httpServer, {
  cors: {
    origin: "*", // 允许跨域的前端域名
    methods: ["GET", "POST"], // 允许的跨域请求方法
    credentials: true, // 允许cookies等认证信息一起跨域传递
  },
});

// 维护一个map,key表示房间号,value表示用户列表,value是一个数组,里面存有socketId,用户ID
const groupList = {};

// 监听连接
io.on("connection", (socket) => {
  // 监听连接事件
  socket.on("join", async ({ roomId, id, userName }) => {
    console.log(userName, "连接到房间");
    // 连接到指定的房间号
    socket.join(roomId);
    // 维护map信息
    if (groupList[roomId]) {
      groupList[roomId].push({
        id, // 用户ID
        userName, // 用户名
        roomId, // 房间号
        socketId: socket.id, // socketId
      });
    } else {
      groupList[roomId] = [{ id, userName, roomId, socketId: socket.id }];
    }
    // 获取群聊总用户
    let countRes = await axios.get(`/user/getUserCount`);
    // 向房间内的所有用户广播消息,更新用户数量
    io.to(roomId).emit("userCount", {
      all: countRes.data,
      online: io.engine.clientsCount, // 获取当前房间的有效连接数
    });
  });

  // 监听发送消息事件
  socket.on("send", async (data) => {
    console.log(data, "接收到消息");

    // 发送消息
    let res = await axios.post(`/message/send`, data);
    // 更新消息
    io.to(data.roomId).emit("message", res.data);
  });

  // 监听用户离开事件
  socket.on("disconnect", async () => {
    // 遍历房间信息,找到是谁离开了,然后更新这个房间的在线人数
    for (let key in groupList) {
      let list = groupList[key];
      // 根据socketId找到用户
      let index = list.findIndex((item) => item.socketId === socket.id);
      if (index !== -1) {
        let userInfo = list[index];
        let countRes = await axios.get(`/user/getUserCount`);
        // 向房间内的所有用户广播消息,更新用户数量
        io.to(userInfo.roomId).emit("userCount", {
          all: countRes.data,
          online: io.engine.clientsCount,
        });
        // 删除用户
        list.splice(index, 1);
        break;
      }
    }
  });
});

httpServer.listen(3001, () => {
  console.log("sockit服务器已启动 ws://localhost:3001");
});

业务代码整合

index.ts

import "reflect-metadata"; // 装饰器的基础,放在顶层
import "./src/message/sockit";
import { InversifyExpressServer } from "inversify-express-utils";
import { Container } from "inversify";
import express from "express";
import { PrismaClient } from "@prisma/client";
import { PrismaDB } from "./db";
import { UserController } from "./src/user/controller";
import { UserService } from "./src/user/service";
import { MessageController } from "./src/message/controller";
import { MessageService } from "./src/message/service";

const container = new Container();
// 注入工厂注入PrismaClient
container.bind<PrismaClient>("PrismaClient").toFactory(() => {
  return () => new PrismaClient();
});
// 注入数据库ORM框架PrismaClient
container.bind(PrismaDB).to(PrismaDB);
// 用户模块
container.bind(UserController).to(UserController);
container.bind(UserService).to(UserService);
// 消息模块
container.bind(MessageController).to(MessageController);
container.bind(MessageService).to(MessageService);

const server = new InversifyExpressServer(container);

server.setConfig((app) => {
  // 配置中间件,允许post参数
  app.use(express.json());
  // 允许跨域
  app.use(function (req, res, next) {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "*");
    res.setHeader("Access-Control-Allow-Methods", "*");
    next();
  });
});

const app = server.build();

app.listen(3000, () => {
  console.log("Server is running on port http://localhost:3000");
});

运行 npm run start

image-20241008142624829

前端代码实现

初始化项目

npm create vue@latest

image-20241008143042164

安装依赖

"dependencies": {
    "axios": "^1.7.7",
    "sass": "^1.79.3",
    "socket.io-client": "^4.8.0"  
},

加入群聊

src/views/join.vue

<template>
  <div class="content">
    <div class="logo">
      <img src="../assets/logo.webp" />
    </div>
    <div class="main">
      <div>请输入您的用户名</div>
      <input
        id="userName"
        type="text"
        placeholder="请输入内容"
        autocomplete="off"
        v-model="userName"
      />
    </div>
    <div class="btn">
      <button @click="join">加入群聊</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import http from "../../utils/http";

const router = useRouter();
const userName = ref("");

async function join() {
  if (!userName.value) {
    alert("请输入用户名");
    return;
  }
  let userInfo = {};
  let res = await http.get(`/user/getUser?userName=${userName.value}`);
  if (res.data) {
    userInfo = res.data;
  } else {
    let res = await http.post(`/user/add`, {
      userName: userName.value,
    });
    userInfo = res.data;
  }

  // 进入聊天页面
  router.push({ path: "/chat", query: userInfo });
}
</script>

<style scoped lang="scss">
.content {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  .logo {
    width: 100px;
    height: 100px;
    margin-top: 30%;
    img {
      width: 100%;
      height: 100%;
    }
  }

  .main {
    width: 80%;
    margin-top: 20%;
    input {
      padding: 10px;
      box-sizing: border-box;
      border: 1px solid #a18cd1;
      border-radius: 5px;
      width: 100%;
      margin-top: 10px;
    }
  }

  .btn {
    width: 80%;
    button {
      padding: 10px;
      border: none;
      border-radius: 5px;
      color: #ffffff;
      margin-top: 30px;
      width: 100%;
      background-image: linear-gradient(-90deg, #a18cd1 0%, #fbc2eb 100%);
    }
  }
}
</style>
image-20241008143924321

点击加入群聊后会先去查询当前用户名是否在表中存在,如果不存在,则会新建一个用户保存到表中,并返回用户信息,如果存在,则直接返回用户信息跳转到聊天界面

群聊实现

src/views/chat.vue

<template>
  <div class="content">
    <div class="header">
      <div>聊天室({{ state.count.all }})</div>
      <div class="online">当前在线人数:{{ state.count.online }}</div>
    </div>
    <div class="main">
      <div v-for="item in state.msgList" :key="item.id">
        <div class="item mymsg" v-if="item.userId === state.userInfo.id">
          <div>{{ item.userName }}</div>
          <div class="msg">{{ item.text }}</div>
        </div>
        <div class="item" v-else>
          <div>{{ item.userName }}</div>
          <div class="msg">{{ item.text }}</div>
        </div>
      </div>
    </div>
    <div class="footer">
      <input
        v-on:keyup.enter="sendMsg"
        type="text"
        placeholder="请输入内容"
        v-model="state.msg"
      />
      <button @click="sendMsg">发送</button>
    </div>
  </div>
</template>

<script setup>
import { nextTick, onMounted, reactive, onUnmounted } from "vue";
import { useRoute } from "vue-router";
import io from "socket.io-client";
import http from "../../utils/http";

// 连接到后端
const socket = io("ws://localhost:3001", {
  transports: ["websocket", "polling"],
  withCredentials: true,
});

const route = useRoute();
const state = reactive({
  msg: "",
  msgList: [],
  count: {
    all: 0,
    online: 0,
  },
  userInfo: {
    id: parseInt(route.query.id),
    userName: route.query.userName,
    roomId: 1, // 房间号暂时固定为1
  },
});

function sendMsg() {
  let sendData = {
    roomId: state.userInfo.roomId,
    userId: state.userInfo.id,
    userName: state.userInfo.userName,
    text: state.msg,
  };

  state.msg = "";

  // 发送消息
  socket.emit("send", sendData);
}

// 获取历史消息
async function getHistoryMsg() {
  const res = await http.get("/message/list");
  state.msgList = res.data;
}

onMounted(() => {
  // 监听连接
  socket.on("connect", async () => {
    // 获取历史消息
    await getHistoryMsg();
    // main 滑动到底部
    nextTick(() => {
      const main = document.querySelector(".main");
      main.scrollTop = main.scrollHeight;
    });
    // 连接到房间
    socket.emit("join", state.userInfo);
    // 更新数量
    socket.on("userCount", (count) => {
      state.count = count;
    });
    // 监听消息
    socket.on("message", (data) => {
      // 将消息添加到列表
      state.msgList.push(data);
      // main 滑动到底部
      nextTick(() => {
        const main = document.querySelector(".main");
        main.scrollTop = main.scrollHeight;
      });
    });
  });
});

// 组件销毁时关闭连接
onUnmounted(() => {
  socket.close();
});
</script>

<style scoped lang="scss">
.content {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  height: 100%;
}
.header {
  height: 45px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  .online {
    font-size: 12px;
    color: #999;
    position: relative;
  }
  .online::before {
    content: "";
    position: absolute;
    width: 10px;
    height: 10px;
    background-color: #09d638;
    border-radius: 50%;
    left: -15px;
    top: 5px;
  }
}

.main {
  height: calc(100% - 95px);
  background-color: #f6f6f6;
  padding: 2%;
  overflow: auto;
  .item {
    margin-bottom: 10px;
    .msg {
      max-width: 80%;
      padding: 10px;
      border-radius: 5px;
      display: inline-block;
      text-align: left;
      margin-top: 2px;
      background-color: #ffffff;
    }
  }
  .mymsg {
    text-align: right;
    .msg{
      background-color: #e6e1f5;
    }
  }
}

.footer {
  height: 50px;
  background-color: #ffffff;
  padding: 5px;
  display: flex;
  box-sizing: border-box;
  align-items: center;

  input {
    flex: 1;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
  }
  button {
    width: 18%;
    padding: 10px;
    border: none;
    border-radius: 50px;
    background-image: linear-gradient(-90deg, #a18cd1 0%, #fbc2eb 100%);
    color: #ffffff;
    margin-left: 2%;
    height: 35px;
  }
}
</style>

image-20241008144431103

使用lottie实现入场动画

我们就简单的根据用户名中是否以vip开头,如果是VIP开头的用户加入群聊后,就播放一个入场动画,主要目的是为了学习 lottie 动画

lottie官网: https://airbnb.io/lottie/#/web

免费动画库:https://lottiefiles.com/free-animation

安装

npm install lottie-web

在chat.vue代码中添加下面的代码

<!-- 动画播放器 -->
<div class="lottie-view" v-if="isPlaying">
    <div id="lottie"></div>
    <div>欢迎 {{ currJoinUser }} 加入群聊</div>
</div>

添加样式

.lottie-view {
  position: absolute;
  top: 10%;
  left: 0;
  text-align: center;
}

编写动画逻辑

import lottie from "lottie-web";
import vipJoin from "../assets/vipjoin.json";

// 动画逻辑
const animation = ref(null);
const isPlaying = ref(false);
const currJoinUser = ref(null);

const toggleAnimation = () => {
  if (isPlaying.value) return;
  isPlaying.value = true;
  nextTick(() => {
    animation.value = lottie.loadAnimation({
      container: document.getElementById("lottie"),
      renderer: "canvas",
      loop: false,
      autoplay: true,
      animationData: vipJoin,
    });
    animation.value.addEventListener("complete", () => {
      isPlaying.value = false;
    });
  });
};

案例中对应的json地址:https://lottiefiles.com/free-animation/success-celebration-Sn1bJRj6pz

然后在 userCount 监听方法中,判断最新加入的用户名是否是vip开头的,如果是就播放动画

// 更新数量
socket.on("userCount", (data) => {
    state.count.all = data.all;
    state.count.online = data.online;
    currJoinUser.value = data.userName;
    // 判断用户名称是否包含vip
    if (data.userName && data.userName.startsWith("vip")) {
        toggleAnimation();
    }
});

同时后端代码中的 src/message/sockit.ts 文件,下面的代码需要修改一下,多传递一个用户名称

// 向房间内的所有用户广播消息,更新用户数量
io.to(roomId).emit("userCount", {
    all: countRes.data,
    online: io.engine.clientsCount,
    userName // 广播最新加入的用户
});

效果展示

lottie2

完整代码

仓库地址

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

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

相关文章

iLogtail 开源两周年:UC 工程师分享日志查询服务建设实践案例

作者&#xff1a;UC 浏览器后端工程师&#xff0c;梁若羽 传统 ELK 方案 众所周知&#xff0c;ELK 中的 E 指的是 ElasticSearch&#xff0c;L 指的是 Logstash&#xff0c;K 指的是 Kibana。Logstash 是功能强大的数据处理管道&#xff0c;提供了复杂的数据转换、过滤和丰富…

如何写好SCI论文的Abstract

摘要是一篇论文的缩影&#xff0c;是对全文内容的高度浓缩和提炼&#xff0c;也是整篇论文的精髓和灵魂。读者通常先通过摘要快速获得文章信息&#xff0c;然后决定是否要进一步仔细阅读全文&#xff0c;因此&#xff0c;写好摘要至关重要! 那么如何才能写好论文摘要呢&#xf…

线性代数在大一计算机课程中的重要性

线性代数在大一计算机课程中的重要性 线性代数是一门研究向量空间、矩阵运算和线性变换的数学学科&#xff0c;在计算机科学中有着广泛的应用。大一的计算机课程中&#xff0c;线性代数的学习为学生们掌握许多计算机领域的关键概念打下了坚实的基础。本文将介绍线性代数的基本…

睡眠小乖 2.2.19 | 免费改善睡眠神器

睡眠小乖是一款完全免费的睡眠监测软件、冥想软件、改善睡眠软件。支持的功能包括&#xff1a;睡眠监测、梦话鼾声记录、睡眠报告、多种白噪音、冥想板块。白噪音板块提供了近80种白噪音&#xff0c;支持任意选择4种白噪音组合成一个混音&#xff0c;支持单独调节每种白噪音的音…

高标准农田建设专项整治行动拉开序幕,建设监管如何破局?

近日&#xff0c;高标准农田建设工程质量“回头看”和专项整治行动拉开序幕&#xff0c;将全面梳理2020年以来立项实施的高标准农田建设项目&#xff0c;围绕前期工作、施工建设、管护利用等环节&#xff0c;聚焦方案设计是否规范、监管责任是否落实、建后利用是否到位、建后管…

安全认证:oath2

一、一些概念&#xff1a; 1、认证&#xff1a;主要解决的是你是谁的问题。 三个层面认证&#xff1a;信道认证&#xff08;SSL等&#xff09;&#xff0c;协议认证&#xff08;例如用http协议的时候的格式&#xff09;&#xff0c;内容认证&#xff08;比如浏览网页的时候&a…

ASO优化截图如何影响 App Store 和 Google Play 的安装数量

无论您的应用程序多么吸引人和有用&#xff0c;用户在下载它之前都不知道它。但是&#xff0c;您如何让潜在用户在众多竞争对手应用程序中选择您的应用呢&#xff1f;关键是让您的应用程序引人注目。在本文中&#xff0c;我将向您介绍应用程序的视觉组件以及如何增强它以从其他…

自主性革命:人工智能赋予人形机器人大脑

大家好&#xff0c;我是Shelly&#xff0c;一个专注于输出AI工具和科技前沿内容的AI应用教练&#xff0c;体验过300款以上的AI应用工具。关注科技及大模型领域对社会的影响10年。关注我一起驾驭AI工具&#xff0c;拥抱AI时代的到来。 人工智能&#xff08;AI&#xff09;是人形…

蛋鸡养殖场饲料粉碎加工机器设备

蛋鸡养殖场饲料粉碎加工机器设备种类繁多&#xff0c;包括粉碎机、混合机、制粒机等多种设备&#xff0c;用于将饲料原料进行粉碎、混合、制粒等处理&#xff0c;以生产出营养均衡、易于消化吸收的蛋鸡饲料‌。具体来说&#xff1a;‌粉碎机‌&#xff1a;用于将谷物、豆类等饲…

安卓手机termux安装ubuntu24桌面环境

要在 proot-distro 中的 Ubuntu 上安装桌面环境并实现 远程连接&#xff0c;可以按照以下步骤进行。这将包括安装轻量级桌面环境&#xff08;如 LXDE 或 XFCE&#xff09;、VNC 服务器&#xff0c;并配置远程访问。 步骤 1&#xff1a;安装 Ubuntu 安装并登录 Ubuntu&#xff1…

数据结构 ——— 单链表oj题:环状链表(判断链表是否带环)

目录 题目要求 手搓简易环状单链表 代码实现 问题1&#xff1a;slow 指针和 fast 指针一定会相遇吗 问题2&#xff1a;slow 每次走一步&#xff0c;fast 每次走 n 步是否还能判断链表带环&#xff1f;&#xff08;n > 2&#xff09; 题目要求 有一个单链表的头节点 …

适合新手的快速搭建全景VR服务器教程

近期有一些朋友在使用BZ全景可视化编辑器的过程中, 不了解如何把全景编辑器生成的静态全景VR HTML项目部署到自己的服务器上, 本篇文章将详细介绍如何使用宝塔面板来搭建一个全景VR服务器 我们将从安装宝塔面板开始&#xff0c;配置静态网页服务器&#xff0c;上传全景静态HTM…

英伟达股价分析:英伟达股价能否上涨到150美元,接下来该如何操作?

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经​ 猛兽财经核心观点&#xff1a; &#xff08;1&#xff09;华尔街投行Oppenheimer已将英伟达的目标价上调到了150美元。 &#xff08;2&#xff09;产品方面的最新进展和合作伙伴关系进一步提升了英伟达的市场地位。 &…

Java生成Excel_低内存占用_更快

EasyExcel&#xff1a;高效Java Excel工具&#xff0c;解决大文件读写难题 EasyExcel是一个基于Java的、快速简洁且能有效解决大文件内存溢出问题的Excel处理工具。它使得用户可以在无需过多关注性能和内存消耗的情况下&#xff0c;轻松实现Excel文件的读写功能。相较于传统的…

微信小程序后台搭建—node+mysql

想必大家都有一个困扰&#xff0c;想要用微信小程序作为前端&#xff0c;但是后端不知道如何用node连接微信小程序&#xff0c;我最近也一直困扰许久&#xff0c;所以我就想用node写后端接口在连接微信小程序&#xff0c;记录一下学习笔记 前言 前端:微信小程序 后端:nodeexpr…

linux mqtt 服务记录

查看已连接的clientId 如果MQTT服务使用的是系统服务管理器&#xff08;如systemd&#xff09;&#xff0c;你可以使用journalctl命令来查看服务的日志 journalctl -u mqtt.service 这里的mqtt.service是MQTT服务的名称&#xff0c;根据实际情况替换为你的服务名称&#xff…

element-plus组件之Upload(2.0)

接上篇 下面的属性就对应着回调函数&#xff0c;下面就一一进行介绍。 因为element-plus在封装upload组件时就自带了一个预览和删除的图标&#xff0c;只是没有方法实现&#xff0c;这里进行指明。 就是在图片墙列表中&#xff0c;自动就带了这两个图标和遮罩&#xff0c;下面…

论文笔记:Prototypical Verbalizer for Prompt-based Few-shot Tuning

论文来源&#xff1a;ACL 2022 论文地址&#xff1a;https://arxiv.org/pdf/2203.09770.pdfhttps://arxiv.org/pdf/2203.09770.pdf 论文代码&#xff1a;https://github.com/thunlp/OpenPrompthttps://github.com/thunlp/OpenPrompt Abstract 基于提示的预训练语言模型&#…

Windows 下 cocos2d-x-3.17.2 VS2017开发环境搭建

1.下载cocos2d-x-3.17.2 源码: Cocos2d-x - 成熟、轻量、开放的跨平台解决方案 2.下载Python2 Python 2.7.0 Release | Python.org 加入环境变量: 测试版本

用无人机视角,打开哀牢山!

哀牢山危险且神秘&#xff0c;使用无人机进行探索可以极大地提高安全性和效率。通过无人机的关键性能&#xff0c;将哀牢山的情况记录并传输出来 一、高清摄像与图像传输 高清摄像头&#xff1a;无人机通常搭载高分辨率的摄像头&#xff0c;能够捕捉到哀牢山细腻的自然景观和…