Django+Nginx+uwsgi网站Channels+redis+daphne多人在线聊天实现粘贴上传图片

ops/2024/11/29 20:03:02/

 在Django+Nginx+uwsgi网站Channels+redis+daphne多人在线的基础上(详见Django+Nginx+uwsgi网站使用Channels+redis+daphne实现简单的多人在线聊天及消息存储功能-CSDN博客),实现在输入框粘贴或打开本地图片,上传到网站后返回图片路径,以链接的形式将图片插入到输入框显示,并实现异步发送消息。具体效果如下图所示:

一、实现图片上传

实现图片上传客户端和服务器两边都要配置。

1.  客户端使用fetch实现图片上传

使用嵌入页面的javascript脚本实现fetch上传图片,主要代码如下:

javascript">        const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;fetch('/chatjson/upload_image/',{method: 'POST',headers: {'X-CSRFToken': csrftoken},body: imgformData}).then(response => response.json()).then(data => {if (data.image_url) {//console.log('data image path::',data.image_url);const img = document.createElement('img');img.src = data.image_url;editor.appendChild(img);img.style.width = '300px';img.style.height = 'auto';img.style.objectFit = 'contain';img.style.float = 'none';} else {console.error('Error uploading image:', data.error);}}).catch((error) => {console.error('Error:', error);alert('Error uploading the image.');});
2. 服务器端配置
(1) urls.py设置

客户端fetch的路径为'/chatjson/upload_image/',需要在urls.py中配置路径解析,包括聊天页面的路径解析

python">from myapp import views as channelsview
urlpatterns = [
....path('chatexp/<str:room_name>/', channelsview.chatexp, name='chatexp'),path('chatjson/upload_image/', channelsview.upload_image_json, name='upload_json'),
]
(2) 视图设置 myapp/views.py

包括聊天页面视图响应函数chatexp和文件上传响应upload_image_json

python">from django.contrib.auth.decorators import login_required@login_required(login_url='/login/')
def chatexp(request,room_name):username = request.session.get('username','游客')msgs = ChatMessage.objects.filter(room=room_name).order_by('-create_time')[0:20]if request.method == 'POST':form = chatimgsForm(request.POST, request.FILES)if form.is_valid():image = form.save()#图片路径image_path = image.image.urlreturn render(request,"channels/chattingexp.html",{'room_name':room_name,'form':form, 'image_path':image_path, 'username':username, 'msgs':msgs})form = chatimgsForm()return render(request,"channels/chattingexp.html",{'room_name':room_name,'form':form, 'image_path':'未上传', 'username':username, 'msgs':msgs})from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
from django.conf import settings
import os@require_POST
@csrf_exempt
def upload_image_json(request):image_file = request.FILES['image']if image_file :upimg = chatimgs(image=image_file)upimg.save()#返回图片的绝对路径/home/...#image_path = upimg.image.path# 返回图片的相对路径/media/...image_path = upimg.image.urlreturn JsonResponse({'image_url': image_path})else:return JsonResponse({'error': 'No image received!!'}, status=400)

二、客户端配置

1. 聊天页面设置

chatexp视图函数调用聊天页面chattingexp.html,聊天页面输入框由可编辑的div实现,页面内javascript脚本监听输入框的粘贴事件,将其中的图片上传,返回路径,将图片以img元素的形式插入到输入框,字符串转换成文本插入。脚本还实现了打开本地图片文件,同样上传后返回路径,将图片以img元素的形式插入到输入框。然后发送消息,消息文本通过channels异步传输,因文本只有图片链接,提高了传输效率。主要代码如下:

    <script>const editor = document.getElementById('chat-message-input'); editor.addEventListener('paste', function(event) {// 阻止默认粘贴操作event.preventDefault();const clipboardData = (event.clipboardData||window.clipboardData);let items = clipboardData.items;const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;for (const item of items) {if  (item.kind === 'string') {item.getAsString((text) => {const regex = /<img src="(.*?)"/;const match = text.match(regex);if (match) {document.execCommand('insertText', false, "link:<img src='"+match[1]+"'/>");//网页图片复制粘贴除了图片还带有图片链接,如果识别img链接插入图片会出现图片插入两次的问题} else {document.execCommand('insertText', false, text);}})} else if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {var imgfile = item.getAsFile();const imgformData = new FormData();imgformData.append('image',imgfile);imgformData.append('csrfmiddlewaretoken', csrftoken);fetch('/chatjson/upload_image/',{method: 'POST',headers: {'X-CSRFToken': csrftoken},body: imgformData}).then(response => response.json()).then(data => {if (data.image_url) {//console.log('data image path::',data.image_url);const img = document.createElement('img');img.src = data.image_url;editor.appendChild(img);img.style.width = '300px';img.style.height = 'auto';img.style.objectFit = 'contain';img.style.float = 'none';} else {console.error('Error uploading image:', data.error);}}).catch((error) => {console.error('Error:', error);alert('Error uploading the image.');});}}})window.onload = function() {var scrollableDiv = document.getElementById('chat-record');// 设置scrollTop使得滚动条向下翻scrollableDiv.scrollTop = scrollableDiv.scrollHeight;};const roomName = JSON.parse(document.getElementById('room-name').textContent);const username = JSON.parse(document.getElementById('username').textContent);const chatSocket = new WebSocket('wss://abc.com/ws/chat/' + roomName + '/');chatSocket.onmessage = function(e) {const data = JSON.parse(e.data);//data为收到的后端发出来的数据//console.log(data);if (data['message']) {if(data['username'] == username){document.querySelector('#chat-record').innerHTML += ('<div class="chat-message right"><div class="user-content">' + data['username'] + '</div><div class="message-content"><span>' +data['message'] + '</span></div></div><br>');}else{document.querySelector('#chat-record').innerHTML += ('<div class="chat-message left"><div class="user-content">' + data['username'] + '</div><div class="message-content"><span>' + data['message'] + '</span></div></div><br>');}} else {alert('消息为空!')}var scrollableDiv = document.getElementById('chat-record');// 设置scrollTop使得滚动条向下翻scrollableDiv.scrollTop = scrollableDiv.scrollHeight;};chatSocket.onclose = function(e) {console.error('聊天端口非正常关闭!');};document.querySelector('#chat-message-input').focus();document.querySelector('#chat-message-input').onkeyup = function(e) {if (e.keyCode === 13) {  // enter, returndocument.querySelector('#chat-message-submit').click();}};document.querySelector('#chat-message-submit').onclick = function(e) {const messageDivDom = document.querySelector('#chat-message-input');const message = messageDivDom.innerHTML;chatSocket.send(JSON.stringify({'message': message,'username':username}));messageDivDom.innerHTML = '';};//打开并上传本地文件document.getElementById('upload-btn').addEventListener('click', function () {const editor = document.getElementById('chat-message-input'); const fileInput = document.getElementById('file-input');const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;const file = fileInput.files[0];const formData = new FormData();formData.append('csrfmiddlewaretoken', csrftoken);formData.append('image', file);fetch('/chatjson/upload_image/', {method: 'POST',headers: {'X-CSRFToken': csrftoken},body: formData}).then(response => response.json()).then(data => {if (data.image_url) {//console.log('data image path::',data.image_url);const img = document.createElement('img');img.src = data.image_url;editor.appendChild(img);img.style.width = '300px';img.style.height = 'auto';img.style.objectFit = 'contain';img.style.float = 'none';} else {console.error('Error uploading image:', data.error);}}).catch((error) => {console.error('Error:', error);alert('Error uploading the image.');});});</script>
2. 网页内容的粘贴处理

在Windows下从网页复制粘贴涉及剪切和粘贴 HTML 文档的片段。Windows采用 CF_HTML 剪贴板格式,其将原始 HTML 文本及其上下文(即外部 HTML)片段作为 ASCII 存储在剪贴板上。 这允许应用程序检查 HTML 片段的上下文,该片段由前面所有的周围标记组成,以便可以使用其属性来记录 HTML 片段的周围标记。 CF_HTML 剪贴板的常规布局或语法,如下所示:

<cf-html>                ::= <description-header> <context>
<context>                ::= [<preceding-context>] <fragment> [<trailing-context>]<description-header>     ::= "Version:" <version> <br> ( <header-offset-keyword> ":" <header-offset-value> <br> )*
<header-offset-keyword>  ::= "StartHTML" | "EndHTML" | "StartFragment" | "EndFragment" | "StartSelection" | "EndSelection"
<header-offset-value>    ::= { Base 10 (decimal) integer string with optional _multiple_ leading zero digits (see "Offset syntax" below) }
<version>                ::= "0.9" | "1.0"
<fragment>               ::= <fragment-start-comment> <fragment-text> <fragment-end-comment>
<fragment-start-comment> ::= "<!--StartFragment -->"
<fragment-end-comment>   ::= "<!--EndFragment -->"
<preceding-context>      ::= { Arbitrary HTML }
<trailing-context>       ::= { Arbitrary HTML }
<fragment-text>          ::= { Arbitrary HTML }
<br>                     ::= "\r" | "\n" | "\r\n"

所以从网页单独粘贴一张图片时,会带入上下文,譬如:

<html>
<body>
<!--StartFragment--><img src="https://pics6.baidu.com/feed/a2cc7cd98d1001e9472d4193277785e255e797a5.jpeg@f_auto?token=475ae4ac49d50e247ac05e958799fc88"/><!--EndFragment-->
</body>
</html>

 如果既上传照片,又解析其中的<img>链接,会出现从网页上拷贝的文字图片在粘贴时,图片会被识别两次。所以对网页拷贝的内容,解决办法有两种:

(1)将图片以链接形式插入到文本中

chattingexp.html中Javascript脚本的主要代码如下:

javascript">    <script>const editor = document.getElementById('chat-message-input'); editor.addEventListener('paste', function(event) {// 阻止默认粘贴操作event.preventDefault();const clipboardData = (event.clipboardData||window.clipboardData);let items = clipboardData.items;const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;for (const item of items) {let htmlimglink = false;if(item.kind === 'string'&& item.type === 'text/html') {item.getAsString((text) => {const regex = /<img src="(.*?)"/;const match = text.match(regex);if (match) {                //网页图片以链接的形式存在,不上传服务器//document.execCommand('insertText', false, "link:<img src='"+match[1]+"'/>");const img = document.createElement('img');img.src = match[1];editor.appendChild(img);img.style.width = '300px';img.style.height = 'auto';img.style.objectFit = 'contain';img.style.float = 'none';htmlimglink = true;} else {//console.log(text);//新建一个divvar divElement = document.createElement( "div" );divElement.innerHTML = text;//获取文本内容//如果divElement是null或undefined,那么返回为""(一个空字符串)。//如果divElement非null且存在textContent或innerText属性,那么将会返回该属性的值。如果两者都不存在,将会返回""。document.execCommand('insertText', false, divElement.textContent || divElement.innerText || "");}})} else if (item.kind === 'string'&& item.type === 'text/plain'){item.getAsString((text) => {document.execCommand('insertText', false, text);})}else if(htmimglink=false && item.kind === 'file' && item.type.indexOf('image/') !== -1) {var imgfile = item.getAsFile();const imgformData = new FormData();imgformData.append('image',imgfile);imgformData.append('csrfmiddlewaretoken', csrftoken);fetch('/chatjson/upload_image/',{method: 'POST',headers: {'X-CSRFToken': csrftoken},body: imgformData}).then(response => response.json()).then(data => {if (data.image_url) {//console.log('data image path::',data.image_url);const img = document.createElement('img');img.src = data.image_url;editor.appendChild(img);img.style.width = '300px';img.style.height = 'auto';img.style.objectFit = 'contain';img.style.float = 'none';} else {console.error('Error uploading image:', data.error);}}).catch((error) => {console.error('Error:', error);alert('Error uploading the image.');});}}})//本地文件上传按钮事件document.getElementById('upload-btn').addEventListener('click', function () {const editor = document.getElementById('chat-message-input'); const fileInput = document.getElementById('file-input');const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;const file = fileInput.files[0];const formData = new FormData();formData.append('csrfmiddlewaretoken', csrftoken);formData.append('image', file);fetch('/chatjson/upload_image/', {method: 'POST',headers: {'X-CSRFToken': csrftoken},body: formData}).then(response => response.json()).then(data => {if (data.image_url) {//console.log('data image path::',data.image_url);const img = document.createElement('img');img.src = data.image_url;editor.appendChild(img);img.style.width = '300px';img.style.height = 'auto';img.style.objectFit = 'contain';img.style.float = 'none';} else {console.error('Error uploading image:', data.error);}}).catch((error) => {console.error('Error:', error);alert('Error uploading the image.');});});</script>

(2)使用fetch获取图片Blob数据后上传到服务器

在前面的基础上,得到图片的链接后,利用正则表达式得到图片名和图片的格式,然后fetch到图片Blob数据后,使用FormData创建一个新的File对象,再使用fetch上传到服务器,返回路径,插入到输入框中。使用效果如下:

3. 聊天页面设计及功能实现

至此,多人在线聊天页面基本实现了粘贴图片和文字混合内容的功能。汇总之前的代码,chattingexp.html的主要内容如下: 

{% extends "newdesign/newbase.html" %}{# 自定义过滤器startswith #}{% load django_bootstrap5 %}{% block mytitle %}<title>{{room_name}}号聊天室</title><style>.chat-window {max-width: 900px;height: 500px;overflow-y: scroll; /* 添加垂直滚动条 */margin: auto;background-color: #f1f1f1;border: 2px solid #09e3f7;border-radius: 5px;padding: 10px;}.chat-message {clear: both;overflow: hidden;margin-bottom: 10px;text-align: left;}.chat-message .message-content {border-radius: 5px;padding: 8px;max-width: 500px;float: left;clear: both;}.chat-message.right .message-content {background-color: #428bca;color: white;float: right;width:420px;}.chat-message.right .user-content {background-color: #f7e91d;border-radius:4px;color: black;float: right;width: auto;text-align: right;padding-left:10px;padding-right:10px;}.chat-message.left .message-content {background-color: #2ef3be;border-color: #ddd;float:left;width:420px;}.chat-message.left .user-content {background-color: #f7e91d;border-radius:4px;border-color: #ddd;float: left;width: auto;text-align: left;padding-left:8px;padding-right:8px;}.inputarea {display:flex;flex-direction: column;justify-content: center;align-items: center;width:900px;margin: 0 auto;}.replyinput {display: inline-block;width: 900px;min-height:120px;background-color: rgb(169, 228, 250);border:2px solid #09e3f7;border-radius: 10px;padding: 10px;font-size: 14px;text-align: left;}.replyarea {width: 900px;height:50px;margin:0 auto;}.sendImg-btn {float:left;border: 0px;background-color: transparent;}.reply-btn {float:right;}</style>{% endblock %}{% block maincontent %} <div class="container"><div id="chat-record" class="chat-window">{% for m in msgs reversed %}{% if m.username == request.user.username %}<div class="chat-message right"><div class="user-content">{{m.username}}</div><div class="message-content"><span>{{m.content|safe}}</span></div></div><br>{% else %}<div class="chat-message left"><div class="user-content">{{m.username}}</div><div class="message-content"><span>{{m.content|safe}}</span></div></div><br>{% endif %}{% endfor %}</div></div><br><form method='post' enctype="multipart/form-data"></form>{% csrf_token %}<div class="inputarea"><divclass="replyinput"contenteditable="true"id="chat-message-input"@focus="onFocusEditableDiv"></div><br><div class="replyarea">&nbsp;&nbsp;<button id="upload-btn">上传本地图片</button>&nbsp;&nbsp;<input type="file" id="file-input" accept="image/*"/><button class="reply-btn" id="chat-message-submit" type="primary" style="height:40px;background-color: #0d4de1;color:white;border-radius: 4px;">发送消息</button></div></div></form>{{ room_name|json_script:"room-name" }}{{ username|json_script:"username" }}
<script>const editor = document.getElementById('chat-message-input'); editor.addEventListener('paste', function(event) {// 阻止默认粘贴操作event.preventDefault();const clipboardData = (event.clipboardData||window.clipboardData);let items = clipboardData.items;const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;var htmlimglink = false;for (const item of items) {if(item.kind === 'string'&& item.type === 'text/html'){//如果是网页内容,因粘贴板格式带有图片链接,直接根据链接下载图片上传,不需要再次上传照片文件htmlimglink = true;}};for (const item of items) {if(item.kind === 'string'&& item.type === 'text/html') {item.getAsString((text) => {const regex = /<img src="(.*?)"/g;let matches = '';matches = text.matchAll(regex);if (matches) {   for (const match of matches) {//网页图片以链接的形式存在,不上传服务器//match[0]为整个匹配组,match[1]为第一个捕获组//document.execCommand('insertHTML', false, "<img style='width:300px;height:auto;object-fit:contain;float:none;' src='"+match[1]+"'/>");fetch(match[1]).then(response =>{if (!response.ok) {throw new Error('Network response was not ok ' + response.statusText);}return response.blob(); // 转换响应为Blob对象}).then(blob => {const fileNameextra = match[1].split('/').pop(); // URL的最后一部分包含文件名和查询参数const regex = /^(.*)\.(png|jpeg|jpg|gif|bmp|webp|svg|tiff|avif)(?:\?|\@|#|$)/i ;let filematches = '';filematches = fileNameextra.match(regex);if(filematches){const fileName = filematches[1]+'.'+ filematches[2];const contentType = 'image/' + filematches[2];//console.log("fileName:::",fileName);const imgformData = new FormData();imgformData.append('image',new File([blob], fileName, { type: contentType }));imgformData.append('csrfmiddlewaretoken', csrftoken);fetch('/chatjson/upload_image/',{method: 'POST',headers: {'X-CSRFToken': csrftoken},body: imgformData}).then(response => response.json()).then(data => {if (data.image_url) {//console.log('data image path::',data.image_url);document.execCommand('insertHTML', false, "<img style='width:300px;height:auto;object-fit:contain;float:none;' src='"+data.image_url+"'/>");} else {console.error('Error uploading image:', data.error);}}).catch((error) => {console.error('Error:', error);alert('Error uploading the image.');});}else{throw new Error('图片格式不正确!');}}).catch(error => console.error('Error fetching or processing the image:', error));};} else {//新建一个divvar divElement = document.createElement( "div" );divElement.innerHTML = text;//获取文本内容//如果divElement是null或undefined,那么返回为""(一个空字符串)。//如果divElement非null且存在textContent或innerText属性,那么将会返回该属性的值。如果两者都不存在,将会返回""。document.execCommand('insertText', false, divElement.textContent || divElement.innerText || "");}})} else if (item.kind === 'string'&& item.type === 'text/plain'){item.getAsString((text) => {document.execCommand('insertText', false, text);})}else if(htmlimglink === false && item.kind === 'file' && item.type.indexOf('image/') !== -1) {var imgfile = item.getAsFile();const imgformData = new FormData();imgformData.append('image',imgfile);imgformData.append('csrfmiddlewaretoken', csrftoken);fetch('/chatjson/upload_image/',{method: 'POST',headers: {'X-CSRFToken': csrftoken},body: imgformData}).then(response => response.json()).then(data => {if (data.image_url) {//console.log('data image path::',data.image_url);const img = document.createElement('img');img.src = data.image_url;editor.appendChild(img);img.style.width = '300px';img.style.height = 'auto';img.style.objectFit = 'contain';img.style.float = 'none';} else {console.error('Error uploading image:', data.error);}}).catch((error) => {console.error('Error:', error);alert('Error uploading the image.');});}}})window.onload = function() {var scrollableDiv = document.getElementById('chat-record');// 设置scrollTop使得滚动条向下翻scrollableDiv.scrollTop = scrollableDiv.scrollHeight;};const roomName = JSON.parse(document.getElementById('room-name').textContent);const username = JSON.parse(document.getElementById('username').textContent);const chatSocket = new WebSocket('wss://abc.com/ws/chat/' + roomName + '/');chatSocket.onmessage = function(e) {const data = JSON.parse(e.data);//data为收到的后端发出来的数据//console.log(data);if (data['message']) {if(data['username'] == username){document.querySelector('#chat-record').innerHTML += ('<div class="chat-message right"><div class="user-content">' + data['username'] + '</div><div class="message-content"><span>' +data['message'] + '</span></div></div><br>');}else{document.querySelector('#chat-record').innerHTML += ('<div class="chat-message left"><div class="user-content">' + data['username'] + '</div><div class="message-content"><span>' + data['message'] + '</span></div></div><br>');}} else {alert('消息为空!')}var scrollableDiv = document.getElementById('chat-record');// 设置scrollTop使得滚动条向下翻scrollableDiv.scrollTop = scrollableDiv.scrollHeight;};chatSocket.onclose = function(e) {console.error('聊天端口非正常关闭!');};document.querySelector('#chat-message-input').focus();document.querySelector('#chat-message-input').onkeyup = function(e) {if (e.keyCode === 13) {  // enter, returndocument.querySelector('#chat-message-submit').click();}};document.querySelector('#chat-message-submit').onclick = function(e) {const messageDivDom = document.querySelector('#chat-message-input');const message = messageDivDom.innerHTML;chatSocket.send(JSON.stringify({'message': message,'username':username}));messageDivDom.innerHTML = '';};document.getElementById('upload-btn').addEventListener('click', function () {const editor = document.getElementById('chat-message-input'); const fileInput = document.getElementById('file-input');const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;const file = fileInput.files[0];const formData = new FormData();formData.append('csrfmiddlewaretoken', csrftoken);formData.append('image', file);fetch('/chatjson/upload_image/', {method: 'POST',headers: {'X-CSRFToken': csrftoken},body: formData}).then(response => response.json()).then(data => {if (data.image_url) {//console.log('data image path::',data.image_url);const img = document.createElement('img');img.src = data.image_url;editor.appendChild(img);img.style.width = '300px';img.style.height = 'auto';img.style.objectFit = 'contain';img.style.float = 'none';} else {console.error('Error uploading image:', data.error);}}).catch((error) => {console.error('Error:', error);alert('Error uploading the image.');});});</script>
{% endblock %}
4. 存在的问题

无法正确处理word内容,拷贝粘贴文字和图片混合内容粘贴显示不太正常,单独复制粘贴图片没问题,文本内容粘贴时需要粘贴为纯文本。


http://www.ppmy.cn/ops/137732.html

相关文章

【H2O2|全栈】Node.js(1)

目录 前言 开篇语 准备工作 ES6导入导出 导入 有名导出 匿名导出 Node概念 Node导入导出 导入 有名导出 匿名导出 Node常用模块 path模块 和路径有关的全局变量 常见方法 导入方法 fs模块 常见方法 导入方法 结束语 前言 开篇语 本系列博客主要分享Java…

AI在线免费视频工具4:AI视频编辑ai-video-composer

1、ai-video-composer 使用自然语言从您的资产组成新的视频。添加视频、图像和音频资源&#xff0c;让 Qwen 2.5-Coder 为您生成一个新的视频(使用 FFMPEG) https://huggingface.co/spaces/huggingface-projects/ai-video-composer

【C/C++】深入解析 Stack 与 Queue 数据结构(详解):实现原理、应用场景与性能优化

文章目录 引言栈&#xff08;Stack&#xff09;数据结构详解1. 栈的基本概念2. 栈的实现原理3. C中的栈实现4. 栈的应用场景5. 栈的性能分析6. 实战示例&#xff1a;括号匹配 队列&#xff08;Queue&#xff09;数据结构详解1. 队列的基本概念2. 队列的实现原理3. C中的队列实现…

Rust vtable(Rust虚表、Rust虚函数表)动态绑定、Rust多态调用、通过类型引用创建trait对象(自动实例化)

文章目录 Rust vtable原理深度解析1. 什么是 vtable&#xff1f;1.1 Trait 对象和 vtableTrait对象指针结构- 一个指向数据的指针&#xff08;指向具体类型实例的数据&#xff09;- 一个指向 vtable 的指针&#xff0c;vtable 存储了该类型所有 trait 方法的函数指针 示例&…

架构第十一章:zabbix

监控体系 1.监控知识概述 &#xff08;1&#xff09;对系统不间断的实时监控 &#xff08;2&#xff09;实时反馈系统和服务状态 &#xff08;3&#xff09;保证系统和服务可靠、安全 &#xff08;4&#xff09;保证业务持续稳定运行 实时 反馈 可靠 安全 2.怎么进行监控&…

跨平台应用开发框架(1)----Qt(组件篇)

目录 1.Qt 1.Qt 的主要特点 2.Qt的使用场景 3.Qt的版本 2.QtSDK 1.Qt SDK 的组成部分 2.安装 Qt SDK 3.Qt SDK 的优势 3.Qt初识 1.快速上手 widget.cpp mian.cpp widget.h Helloworld.pro 2.对象树 3.坐标系 4.信号和槽 1. 信号和槽的基本概念 2. 信号和槽的…

[蓝桥杯 2021 省 AB2] 小平方

题目描述 小蓝发现&#xff0c;对于一个正整数 nn 和一个小于 nn 的正整数 vv&#xff0c;将 vv 平方后对 nn 取余可能小于 nn 的一半&#xff0c;也可能大于等于 nn 的一半。 请问&#xff0c;在 11 到 n−1n−1 中, 有多少个数平方后除以 nn 的余数小于 nn 的一半。 例如&…

使用Eureka实现服务注册与发现的具体案例详解

1. Eureka 的基本概念 1.1 什么是 Eureka&#xff1f; Eureka 是一个基于 REST 的服务注册和发现平台&#xff0c;主要分为以下两个组件&#xff1a; Eureka Server&#xff1a;作为服务注册中心&#xff0c;负责维护服务实例信息。Eureka Client&#xff1a;服务消费者与服…